diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a0a9f9ef5..d3befbc81 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Create a bug report to help us improve -labels: [bug] +labels: [bug, needs triage] body: - type: markdown attributes: @@ -18,6 +18,8 @@ body: required: true - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." required: true + - label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed." + required: true - label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise." required: true - label: "This issue contains only one bug." @@ -40,7 +42,7 @@ body: label: Steps to reproduce the bug description: | What did you do for the bug to show up? - + If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. placeholder: | 1. Go to '...' @@ -69,11 +71,11 @@ body: label: Screenshots/Screen recordings description: | A picture or video is worth a thousand words. - + If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the text box. If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead. - + :heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE. Instead, follow the instructions in the "Logs" section below. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 83d6f0299..52b2a4241 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Suggest an idea for this project -labels: [enhancement] +labels: [enhancement, needs triage] body: - type: markdown attributes: @@ -8,7 +8,6 @@ body: Thank you for helping to make NewPipe better by suggesting a feature. :hugs: Your ideas are highly welcome! The app is made for you, the users, after all. - - type: checkboxes id: checklist attributes: @@ -16,6 +15,8 @@ body: options: - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." required: true + - label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed." + required: true - label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)." required: true - label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise." @@ -43,7 +44,7 @@ body: Describe any problem or limitation you come across while using the app which would be solved by this feature. validations: required: true - + - type: textarea id: additional-information attributes: diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 4c42ab26a..134f171f7 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,6 +1,6 @@ name: Question description: Ask about anything NewPipe-related -labels: [question] +labels: [question, needs triage] body: - type: markdown attributes: @@ -16,6 +16,8 @@ body: options: - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." required: true + - label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed." + required: true - label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise." required: true - label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." @@ -27,7 +29,7 @@ body: label: What is/are your question(s)? validations: required: true - + - type: textarea id: additional-information attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 10e40af2a..abc1665eb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,7 @@ - -#### APK testing +#### APK testing The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 306b8c2c8..d9342e72a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: branches: - dev - master - - release/** + - release** paths-ignore: - 'README.md' - 'doc/**' @@ -31,6 +31,10 @@ on: jobs: build-and-test-jvm: runs-on: ubuntu-latest + + permissions: + contents: read + steps: - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 @@ -64,6 +68,10 @@ jobs: matrix: # api-level 19 is min sdk, but throws errors related to desugaring api-level: [ 21, 29 ] + + permissions: + contents: read + steps: - uses: actions/checkout@v3 @@ -81,7 +89,7 @@ jobs: # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160 emulator-build: 7425822 script: ./gradlew connectedCheck --stacktrace - + - name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 uses: actions/upload-artifact@v3 if: failure() @@ -91,6 +99,10 @@ jobs: sonar: runs-on: ubuntu-latest + + permissions: + contents: read + steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/image-minimizer.yml b/.github/workflows/image-minimizer.yml index c6ab6d5b3..b8bf9e1d2 100644 --- a/.github/workflows/image-minimizer.yml +++ b/.github/workflows/image-minimizer.yml @@ -6,6 +6,10 @@ on: issues: types: [opened, edited] +permissions: + issues: write + pull-requests: write + jobs: try-minimize: runs-on: ubuntu-latest diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index 54e749dc0..b3495135f 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml @@ -9,6 +9,10 @@ on: # Run daily at midnight. - cron: '0 0 * * *' +permissions: + issues: write + pull-requests: write + jobs: noResponse: runs-on: ubuntu-latest @@ -17,4 +21,4 @@ jobs: with: token: ${{ github.token }} daysUntilClose: 14 - responseRequiredLabel: waiting-for-author + responseRequiredLabel: waiting for author diff --git a/README.md b/README.md index c47f8c2f4..52e6eef1a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

NewPipe

-

A libre lightweight streaming frontend for Android.

+

A libre lightweight streaming front-end for Android.

Get it on F-Droid

@@ -13,15 +13,15 @@


-

ScreenshotsDescriptionFeaturesInstallation and updatesContributionDonateLicense

+

ScreenshotsSupported ServicesDescriptionFeaturesInstallation and updatesContributionDonateLicense

WebsiteBlogFAQPress


*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).* -WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. +WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE. -PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS. +PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS. ## Screenshots @@ -38,62 +38,66 @@ [](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png) [](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png) +### Supported Services + +NewPipe currently supports these services: + + +* YouTube ([website](https://www.youtube.com/)) and YouTube Music ([website](https://music.youtube.com/)) ([wiki](https://en.wikipedia.org/wiki/YouTube)) +* PeerTube ([website](https://joinpeertube.org/)) and all its instances (open the website to know what that means!) ([wiki](https://en.wikipedia.org/wiki/PeerTube)) +* Bandcamp ([website](https://bandcamp.com/)) ([wiki](https://en.wikipedia.org/wiki/Bandcamp)) +* SoundCloud ([website](https://soundcloud.com/)) ([wiki](https://en.wikipedia.org/wiki/SoundCloud)) +* media.ccc.de ([website](https://media.ccc.de/)) ([wiki](https://en.wikipedia.org/wiki/Chaos_Computer_Club)) + +As you can see, NewPipe supports multiple video and audio services. Though it started off with YouTube, other people have added more services over the years, making NewPipe more and more versatile! + +Partially due to circumstance, and partially due to its popularity, YouTube is the best supported out of these services. If you use or are familiar with any of these other services, please help us improve support for them! We're looking for maintainers for SoundCloud and PeerTube. + +If you intend to add a new service, please get in touch with us first! Our [docs](https://teamnewpipe.github.io/documentation/) provide more information on how a new service can be added to the app and to the [NewPipe Extractor](https://github.com/TeamNewPipe/NewPipeExtractor). + ## Description -NewPipe does not use any Google framework libraries, nor the YouTube API. Websites are only parsed to fetch required info, so this app can be used on devices without Google services installed. Also, you don't need a YouTube account to use NewPipe, which is copylefted libre software. +NewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe. + +Also, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed. ### Features -* Search videos -* No Login Required -* Display general info about videos -* Watch YouTube videos -* Listen to YouTube videos -* Popup mode (floating player) -* Select streaming player to watch video with -* Download videos -* Download audio only -* Open a video in Kodi -* Show next/related videos -* Search YouTube in a specific language -* Watch/Block age restricted material -* Display general info about channels -* Search channels -* Watch videos from a channel -* Orbot/Tor support (not yet directly) -* 1080p/2K/4K support -* View history -* Subscribe to channels -* Search history -* Search/watch playlists -* Watch as enqueued playlists -* Enqueue videos -* Local playlists -* Subtitles -* Livestream support -* Show comments +* Watch videos at resolutions up to 4K +* Listen to audio in the background, only loading the audio stream to save data +* Popup mode (floating player, aka Picture-in-Picture) +* Watch live streams +* Show/hide subtitles/closed captions +* Search videos and audios (on YouTube, you can specify the content language as well) +* Enqueue videos (and optionally save them as local playlists) +* Show/hide general information about videos (such as description and tags) +* Show/hide next/related videos +* Show/hide comments +* Search videos, audios, channels, playlists and albums +* Browse videos and audios within a channel +* Subscribe to channels (yes, without logging into any account!) +* Get notifications about new videos from channels you're subscribed to +* Create and edit channel groups (for easier browsing and management) +* Browse video feeds generated from your channel groups +* View and search your watch history +* Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing) +* Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service) +* Download videos/audios/subtitles (closed captions) +* Open in Kodi +* Watch/Block age-restricted material -### Supported Services - -NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are: - -* YouTube -* SoundCloud \[beta\] -* media.ccc.de \[beta\] -* PeerTube instances \[beta\] -* Bandcamp \[beta\] - - + ## Installation and updates You can install NewPipe using one of the following methods: 1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/ 2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. - 3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. + 3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users. 4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. + 5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up. -We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. +We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK. In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure: 1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists @@ -101,30 +105,29 @@ In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's 3. Download the APK from the new source and install it 4. Import the data from step 1 via Settings > Content > Import Database -## Contribution -Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. -The more is done the better it gets! +Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing. -If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). +## Contribution +Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). Translation status ## Donate -If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate). +If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as it is both open-source and non-profit. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate). - - - - - + + + + + @@ -134,14 +137,9 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin ## Privacy Policy -The NewPipe project aims to provide a private, anonymous experience for using media web services. -Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/). +The NewPipe project aims to provide a private, anonymous experience for using web-based media services. Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or leave a comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/). ## License [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html) -NewPipe is Free Software: You can use, study, share, and improve it at -will. Specifically you can redistribute and/or modify it under the terms of the -[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as -published by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. +NewPipe is Free Software: You can use, study, share, and improve it at will. Specifically you can redistribute and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/app/build.gradle b/app/build.gradle index b297d5754..9d941d5a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,15 +14,12 @@ android { defaultConfig { applicationId "org.schabi.newpipe" resValue "string", "app_name", "NewPipe" - minSdk 19 + minSdk 21 targetSdk 29 - versionCode 989 - versionName "0.23.3" - - multiDexEnabled true + versionCode 990 + versionName "0.24.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true javaCompileOptions { annotationProcessorOptions { @@ -98,14 +95,14 @@ android { } ext { - checkstyleVersion = '10.0' + checkstyleVersion = '10.3.1' - androidxLifecycleVersion = '2.3.1' - androidxRoomVersion = '2.4.2' + androidxLifecycleVersion = '2.5.1' + androidxRoomVersion = '2.4.3' androidxWorkVersion = '2.7.1' icepickVersion = '3.2.0' - exoPlayerVersion = '2.17.1' + exoPlayerVersion = '2.18.1' googleAutoServiceVersion = '1.0.1' groupieVersion = '2.10.1' markwonVersion = '4.6.2' @@ -113,7 +110,7 @@ ext { leakCanaryVersion = '2.5' stethoVersion = '1.6.0' mockitoVersion = '4.0.0' - assertJVersion = '3.22.0' + assertJVersion = '3.23.1' } configurations { @@ -182,7 +179,7 @@ sonarqube { dependencies { /** Desugaring **/ - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6' /** NewPipe libraries **/ // You can use a local version by uncommenting a few lines in settings.gradle @@ -190,27 +187,27 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:6a858368c86bc9a55abee586eb6c733e86c26b97' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c710da160f488bb40ab2cf4469bec9bd4cefd38' + implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" - ktlint 'com.pinterest:ktlint:0.44.0' + ktlint 'com.pinterest:ktlint:0.45.2' /** Kotlin **/ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" /** AndroidX **/ - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation 'androidx.core:core-ktx:1.6.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'androidx.fragment:fragment-ktx:1.3.6' - implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" - implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" + implementation 'androidx.fragment:fragment-ktx:1.4.1' + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}" implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' - implementation 'androidx.media:media:1.5.0' - implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.media:media:1.6.0' implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.room:room-runtime:${androidxRoomVersion}" @@ -220,10 +217,9 @@ dependencies { // Newer version specified to prevent accessibility regressions with RecyclerView, see: // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' - implementation 'androidx.webkit:webkit:1.4.0' implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.6.1' /** Third-party libraries **/ // Instance state boilerplate elimination @@ -234,11 +230,16 @@ dependencies { implementation "org.jsoup:jsoup:1.15.3" // HTTP client - //noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users - implementation "com.squareup.okhttp3:okhttp:3.12.13" + implementation "com.squareup.okhttp3:okhttp:4.10.0" // Media player - implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}" implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" // Metadata generator for service descriptors @@ -257,9 +258,6 @@ dependencies { implementation "io.noties.markwon:core:${markwonVersion}" implementation "io.noties.markwon:linkify:${markwonVersion}" - // File picker - implementation "com.nononsenseapps:filepicker:4.2.1" - // Crash reporting implementation "ch.acra:acra-core:5.9.3" @@ -273,7 +271,7 @@ dependencies { implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" // Date and time formatting - implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final" + implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final" /** Debugging **/ // Memory leak detection diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 4a54d8992..5e10d3916 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,7 +18,6 @@ -dontobfuscate -keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } --keep class org.ocpsoft.prettytime.i18n.** { *; } -keep class org.mozilla.javascript.** { *; } @@ -26,9 +25,6 @@ -keep class com.google.android.exoplayer2.** { *; } -dontwarn org.mozilla.javascript.tools.** --dontwarn android.arch.util.paging.CountedDataSource --dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource - # Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick -dontwarn icepick.** @@ -39,12 +35,11 @@ } -keepnames class * { @icepick.State *;} -# Rules for OkHttp. Copy paste from https://github.com/square/okhttp +## Rules for OkHttp. Copy paste from https://github.com/square/okhttp -dontwarn okhttp3.** -dontwarn okio.** --dontwarn javax.annotation.** -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase +## + -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; !static !transient ; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f9c99819c..04e28c1ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,7 @@ diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java index 639443377..8d87e90bd 100644 --- a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java +++ b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java @@ -282,11 +282,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt @Nullable public Parcelable saveState() { Bundle state = null; - if (mSavedState.size() > 0) { + if (!mSavedState.isEmpty()) { state = new Bundle(); - final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; - mSavedState.toArray(fss); - state.putParcelableArray("states", fss); + state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0])); } for (int i = 0; i < mFragments.size(); i++) { final Fragment f = mFragments.get(i); diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java index 3e5f408f7..52754e8fa 100644 --- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java +++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java @@ -14,7 +14,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import org.schabi.newpipe.R; import java.lang.reflect.Field; -import java.util.Arrays; import java.util.List; // See https://stackoverflow.com/questions/56849221#57997489 @@ -27,7 +26,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior { private boolean allowScroll = true; private final Rect globalRect = new Rect(); - private final List skipInterceptionOfElements = Arrays.asList( + private final List skipInterceptionOfElements = List.of( R.id.itemsListPanel, R.id.playbackSeekBar, R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); @@ -67,7 +66,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior { public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, @NonNull final AppBarLayout child, @NonNull final MotionEvent ev) { - for (final Integer element : skipInterceptionOfElements) { + for (final int element : skipInterceptionOfElements) { final View view = child.findViewById(element); if (view != null) { final boolean visible = view.getGlobalVisibleRect(globalRect); @@ -132,8 +131,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior { try { final Class headerBehaviorType = this.getClass().getSuperclass().getSuperclass(); if (headerBehaviorType != null) { - final Field field - = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef"); + final Field field = + headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef"); field.setAccessible(true); return field; } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 70c947478..f4410a31b 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -1,5 +1,6 @@ package org.schabi.newpipe; +import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; @@ -7,7 +8,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationManagerCompat; -import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; import com.jakewharton.processphoenix.ProcessPhoenix; @@ -27,9 +27,8 @@ import org.schabi.newpipe.util.StateSaver; import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketException; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Objects; import io.reactivex.rxjava3.exceptions.CompositeException; import io.reactivex.rxjava3.exceptions.MissingBackpressureException; @@ -56,7 +55,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins; * along with NewPipe. If not, see . */ -public class App extends MultiDexApplication { +public class App extends Application { public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; private static final String TAG = App.class.toString(); private static App app; @@ -140,7 +139,7 @@ public class App extends MultiDexApplication { if (throwable instanceof UndeliverableException) { // As UndeliverableException is a wrapper, // get the cause of it to get the "real" exception - actualThrowable = throwable.getCause(); + actualThrowable = Objects.requireNonNull(throwable.getCause()); } else { actualThrowable = throwable; } @@ -149,7 +148,7 @@ public class App extends MultiDexApplication { if (actualThrowable instanceof CompositeException) { errors = ((CompositeException) actualThrowable).getExceptions(); } else { - errors = Collections.singletonList(actualThrowable); + errors = List.of(actualThrowable); } for (final Throwable error : errors) { @@ -213,41 +212,37 @@ public class App extends MultiDexApplication { private void initNotificationChannels() { // Keep the importance below DEFAULT to avoid making noise on every notification update for // the main and update channels - final List notificationChannelCompats = new ArrayList<>(); - notificationChannelCompats.add(new NotificationChannelCompat - .Builder(getString(R.string.notification_channel_id), + final List notificationChannelCompats = List.of( + new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id), NotificationManagerCompat.IMPORTANCE_LOW) - .setName(getString(R.string.notification_channel_name)) - .setDescription(getString(R.string.notification_channel_description)) - .build()); - - notificationChannelCompats.add(new NotificationChannelCompat - .Builder(getString(R.string.app_update_notification_channel_id), + .setName(getString(R.string.notification_channel_name)) + .setDescription(getString(R.string.notification_channel_description)) + .build(), + new NotificationChannelCompat + .Builder(getString(R.string.app_update_notification_channel_id), NotificationManagerCompat.IMPORTANCE_LOW) - .setName(getString(R.string.app_update_notification_channel_name)) - .setDescription(getString(R.string.app_update_notification_channel_description)) - .build()); - - notificationChannelCompats.add(new NotificationChannelCompat - .Builder(getString(R.string.hash_channel_id), + .setName(getString(R.string.app_update_notification_channel_name)) + .setDescription( + getString(R.string.app_update_notification_channel_description)) + .build(), + new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id), NotificationManagerCompat.IMPORTANCE_HIGH) - .setName(getString(R.string.hash_channel_name)) - .setDescription(getString(R.string.hash_channel_description)) - .build()); - - notificationChannelCompats.add(new NotificationChannelCompat - .Builder(getString(R.string.error_report_channel_id), + .setName(getString(R.string.hash_channel_name)) + .setDescription(getString(R.string.hash_channel_description)) + .build(), + new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id), NotificationManagerCompat.IMPORTANCE_LOW) - .setName(getString(R.string.error_report_channel_name)) - .setDescription(getString(R.string.error_report_channel_description)) - .build()); - - notificationChannelCompats.add(new NotificationChannelCompat - .Builder(getString(R.string.streams_notification_channel_id), - NotificationManagerCompat.IMPORTANCE_DEFAULT) - .setName(getString(R.string.streams_notification_channel_name)) - .setDescription(getString(R.string.streams_notification_channel_description)) - .build()); + .setName(getString(R.string.error_report_channel_name)) + .setDescription(getString(R.string.error_report_channel_description)) + .build(), + new NotificationChannelCompat + .Builder(getString(R.string.streams_notification_channel_id), + NotificationManagerCompat.IMPORTANCE_DEFAULT) + .setName(getString(R.string.streams_notification_channel_name)) + .setDescription( + getString(R.string.streams_notification_channel_description)) + .build() + ); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); notificationManager.createNotificationChannelsCompat(notificationChannelCompats); diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 1a3a8adee..9ddbe96df 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -1,7 +1,6 @@ package org.schabi.newpipe; import android.content.Context; -import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -12,40 +11,27 @@ import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Request; import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.util.CookieUtils; import org.schabi.newpipe.util.InfoCache; -import org.schabi.newpipe.util.TLSSocketFactoryCompat; import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import okhttp3.CipherSuite; -import okhttp3.ConnectionSpec; import okhttp3.OkHttpClient; import okhttp3.RequestBody; import okhttp3.ResponseBody; -import static org.schabi.newpipe.MainActivity.DEBUG; - public final class DownloaderImpl extends Downloader { - public static final String USER_AGENT - = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; - public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY - = "youtube_restricted_mode_key"; + public static final String USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; + public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY = + "youtube_restricted_mode_key"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; public static final String YOUTUBE_DOMAIN = "youtube.com"; @@ -54,9 +40,6 @@ public final class DownloaderImpl extends Downloader { private final OkHttpClient client; private DownloaderImpl(final OkHttpClient.Builder builder) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { - enableModernTLS(builder); - } this.client = builder .readTimeout(30, TimeUnit.SECONDS) // .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), @@ -81,69 +64,16 @@ public final class DownloaderImpl extends Downloader { return instance; } - /** - * Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken - * from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_). - *

- * If there is an error, the function will safely fall back to doing nothing - * and printing the error to the console. - *

- * - * @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place) - */ - private static void enableModernTLS(final OkHttpClient.Builder builder) { - try { - // get the default TrustManager - final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new IllegalStateException("Unexpected default trust managers:" - + Arrays.toString(trustManagers)); - } - final X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; - - // insert our own TLSSocketFactory - final SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance(); - - builder.sslSocketFactory(sslSocketFactory, trustManager); - - // This will try to enable all modern CipherSuites(+2 more) - // that are supported on the device. - // Necessary because some servers (e.g. Framatube.org) - // don't support the old cipher suites. - // https://github.com/square/okhttp/issues/4053#issuecomment-402579554 - final List cipherSuites = - new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites()); - cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); - cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); - final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .cipherSuites(cipherSuites.toArray(new CipherSuite[0])) - .build(); - - builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT)); - } catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { - if (DEBUG) { - e.printStackTrace(); - } - } - } - public String getCookies(final String url) { - final List resultCookies = new ArrayList<>(); - if (url.contains(YOUTUBE_DOMAIN)) { - final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY); - if (youtubeCookie != null) { - resultCookies.add(youtubeCookie); - } - } + final String youtubeCookie = url.contains(YOUTUBE_DOMAIN) + ? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null; + // Recaptcha cookie is always added TODO: not sure if this is necessary - final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY); - if (recaptchaCookie != null) { - resultCookies.add(recaptchaCookie); - } - return CookieUtils.concatCookies(resultCookies); + return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY)) + .filter(Objects::nonNull) + .flatMap(cookies -> Arrays.stream(cookies.split("; *"))) + .distinct() + .collect(Collectors.joining("; ")); } public String getCookie(final String key) { @@ -203,7 +133,7 @@ public final class DownloaderImpl extends Downloader { RequestBody requestBody = null; if (dataToSend != null) { - requestBody = RequestBody.create(null, dataToSend); + requestBody = RequestBody.create(dataToSend); } final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder() diff --git a/app/src/main/java/org/schabi/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java index 8da22db2d..bd1351f0c 100644 --- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java @@ -3,7 +3,6 @@ package org.schabi.newpipe; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import org.schabi.newpipe.util.NavigationHelper; @@ -44,11 +43,7 @@ public class ExitActivity extends Activity { protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - finishAndRemoveTask(); - } else { - finish(); - } + finishAndRemoveTask(); NavigationHelper.restartApp(this); } diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index fcb9d9725..d4b2305c7 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -28,7 +28,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -86,7 +85,6 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; -import org.schabi.newpipe.util.TLSSocketFactoryCompat; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -131,11 +129,6 @@ public class MainActivity extends AppCompatActivity { + "savedInstanceState = [" + savedInstanceState + "]"); } - // enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { - TLSSocketFactoryCompat.setAsDefault(); - } - ThemeHelper.setDayNightMode(this); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); @@ -381,8 +374,7 @@ public class MainActivity extends AppCompatActivity { private void showServices() { for (final StreamingService s : NewPipe.getServices()) { - final String title = s.getServiceInfo().getName() - + (ServiceHelper.isBeta(s) ? " (beta)" : ""); + final String title = s.getServiceInfo().getName(); final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) @@ -390,7 +382,7 @@ public class MainActivity extends AppCompatActivity { // peertube specifics if (s.getServiceId() == 3) { - enhancePeertubeMenu(s, menuItem); + enhancePeertubeMenu(menuItem); } } drawerLayoutBinding.navigation.getMenu() @@ -398,9 +390,9 @@ public class MainActivity extends AppCompatActivity { .setChecked(true); } - private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) { + private void enhancePeertubeMenu(final MenuItem menuItem) { final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance(); - menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); + menuItem.setTitle(currentInstance.getName()); final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this)) .getRoot(); final List instances = PeertubeHelper.getInstanceList(this); @@ -480,8 +472,8 @@ public class MainActivity extends AppCompatActivity { ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e); } - final SharedPreferences sharedPreferences - = PreferenceManager.getDefaultSharedPreferences(this); + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this); if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (DEBUG) { Log.d(TAG, "Theme has changed, recreating activity..."); @@ -653,8 +645,8 @@ public class MainActivity extends AppCompatActivity { } super.onCreateOptionsMenu(menu); - final Fragment fragment - = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); + final Fragment fragment = + getSupportFragmentManager().findFragmentById(R.id.fragment_holder); if (!(fragment instanceof SearchFragment)) { toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE); } diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java index b4fbdfb28..f0d1af81a 100644 --- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java +++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java @@ -3,7 +3,6 @@ package org.schabi.newpipe; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; -import android.os.Build; import android.os.Bundle; /* @@ -40,10 +39,6 @@ public class PanicResponderActivity extends Activity { ExitActivity.exitAndRemoveFromRecentApps(this); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - finishAndRemoveTask(); - } else { - finish(); - } + finishAndRemoveTask(); } } diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java index c7604e512..7c646d0e4 100644 --- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java +++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java @@ -16,7 +16,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.SparseItemUtil; -import java.util.Collections; +import java.util.List; public final class QueueItemMenuUtil { private QueueItemMenuUtil() { @@ -53,7 +53,7 @@ public final class QueueItemMenuUtil { case R.id.menu_item_append_playlist: PlaylistDialog.createCorrespondingDialog( context, - Collections.singletonList(new StreamEntity(item)), + List.of(new StreamEntity(item)), dialog -> dialog.show( fragmentManager, "QueueItemMenuUtil@append_playlist" diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 1fe6ce7ec..936beecff 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -30,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.NotificationCompat; import androidx.core.app.ServiceCompat; +import androidx.core.math.MathUtils; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; @@ -60,7 +61,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.local.dialog.PlaylistDialog; -import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; @@ -81,7 +82,6 @@ import org.schabi.newpipe.views.FocusOverlayView; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import icepick.Icepick; @@ -452,7 +452,7 @@ public class RouterActivity extends AppCompatActivity { } } - selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1); + selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size() - 1); if (selectedRadioPosition != -1) { ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); } @@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity { } // ...the player is not running or in normal Video-mode/type - final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); - return playerType == null || playerType == MainPlayer.PlayerType.VIDEO; + final PlayerType playerType = PlayerHolder.getInstance().getType(); + return playerType == null || playerType == PlayerType.MAIN; } private void openAddToPlaylistDialog() { @@ -649,7 +649,7 @@ public class RouterActivity extends AppCompatActivity { .subscribe( info -> PlaylistDialog.createCorrespondingDialog( getThemeWrapperContext(), - Collections.singletonList(new StreamEntity(info)), + List.of(new StreamEntity(info)), playlistDialog -> { playlistDialog.setOnDismissListener(dialog -> finish()); diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt index c816d78be..f19ecd74a 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt @@ -8,7 +8,6 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import io.reactivex.rxjava3.disposables.CompositeDisposable import org.schabi.newpipe.R -import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense import org.schabi.newpipe.databinding.FragmentLicensesBinding import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt index c1dd38389..6e3aa4be8 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt @@ -12,126 +12,92 @@ import org.schabi.newpipe.R import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.external_communication.ShareUtils -import java.io.BufferedReader import java.io.IOException -import java.io.InputStreamReader -import java.nio.charset.StandardCharsets -object LicenseFragmentHelper { - /** - * @param context the context to use - * @param license the license - * @return String which contains a HTML formatted license page - * styled according to the context's theme - */ - private fun getFormattedLicense(context: Context, license: License): String { - val licenseContent = StringBuilder() - val webViewData: String - try { - BufferedReader( - InputStreamReader( - context.assets.open(license.filename), - StandardCharsets.UTF_8 - ) - ).use { `in` -> - var str: String? - while (`in`.readLine().also { str = it } != null) { - licenseContent.append(str) - } +/** + * @param context the context to use + * @param license the license + * @return String which contains a HTML formatted license page + * styled according to the context's theme + */ +private fun getFormattedLicense(context: Context, license: License): String { + try { + return context.assets.open(license.filename).bufferedReader().use { it.readText() } + // split the HTML file and insert the stylesheet into the HEAD of the file + .replace("", "") + } catch (e: IOException) { + throw IllegalArgumentException("Could not get license file: ${license.filename}", e) + } +} - // split the HTML file and insert the stylesheet into the HEAD of the file - webViewData = "$licenseContent".replace( - "", - "" - ) - } - } catch (e: IOException) { - throw IllegalArgumentException( - "Could not get license file: " + license.filename, e - ) +/** + * @param context the Android context + * @return String which is a CSS stylesheet according to the context's theme + */ +private fun getLicenseStylesheet(context: Context): String { + val isLightTheme = ThemeHelper.isLightThemeSelected(context) + val licenseBackgroundColor = getHexRGBColor( + context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color + ) + val licenseTextColor = getHexRGBColor( + context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color + ) + val youtubePrimaryColor = getHexRGBColor( + context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color + ) + return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" + + "a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}" +} + +/** + * Cast R.color to a hexadecimal color value. + * + * @param context the context to use + * @param color the color number from R.color + * @return a six characters long String with hexadecimal RGB values + */ +private fun getHexRGBColor(context: Context, color: Int): String { + return context.getString(color).substring(3) +} + +fun showLicense(context: Context?, component: SoftwareComponent): Disposable { + return showLicense(context, component.license) { + setPositiveButton(R.string.dismiss) { dialog, _ -> + dialog.dismiss() } - return webViewData - } - - /** - * @param context the Android context - * @return String which is a CSS stylesheet according to the context's theme - */ - private fun getLicenseStylesheet(context: Context): String { - val isLightTheme = ThemeHelper.isLightThemeSelected(context) - return ( - "body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor( - context, - if (isLightTheme) R.color.light_license_background_color - else R.color.dark_license_background_color - ) + ";" + "color:#" + getHexRGBColor( - context, - if (isLightTheme) R.color.light_license_text_color - else R.color.dark_license_text_color - ) + "}" + "a[href]{color:#" + getHexRGBColor( - context, - if (isLightTheme) R.color.light_youtube_primary_color - else R.color.dark_youtube_primary_color - ) + "}" + "pre{white-space:pre-wrap}" - ) - } - - /** - * Cast R.color to a hexadecimal color value. - * - * @param context the context to use - * @param color the color number from R.color - * @return a six characters long String with hexadecimal RGB values - */ - private fun getHexRGBColor(context: Context, color: Int): String { - return context.getString(color).substring(3) - } - - fun showLicense(context: Context?, license: License): Disposable { - return showLicense(context, license) { alertDialog -> - alertDialog.setPositiveButton(R.string.ok) { dialog, _ -> - dialog.dismiss() - } - } - } - - fun showLicense(context: Context?, component: SoftwareComponent): Disposable { - return showLicense(context, component.license) { alertDialog -> - alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ -> - dialog.dismiss() - } - alertDialog.setNeutralButton(R.string.open_website_license) { _, _ -> - ShareUtils.openUrlInBrowser(context!!, component.link) - } - } - } - - private fun showLicense( - context: Context?, - license: License, - block: (AlertDialog.Builder) -> Unit - ): Disposable { - return if (context == null) { - Disposable.empty() - } else { - Observable.fromCallable { getFormattedLicense(context, license) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { formattedLicense -> - val webViewData = Base64.encodeToString( - formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING - ) - val webView = WebView(context) - webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") - - AlertDialog.Builder(context).apply { - setTitle(license.name) - setView(webView) - Localization.assureCorrectAppLanguage(context) - block(this) - show() - } - } + setNeutralButton(R.string.open_website_license) { _, _ -> + ShareUtils.openUrlInBrowser(context!!, component.link) } } } + +fun showLicense(context: Context?, license: License) = showLicense(context, license) { + setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } +} + +private fun showLicense( + context: Context?, + license: License, + block: AlertDialog.Builder.() -> AlertDialog.Builder +): Disposable { + return if (context == null) { + Disposable.empty() + } else { + Observable.fromCallable { getFormattedLicense(context, license) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { formattedLicense -> + val webViewData = + Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING) + val webView = WebView(context) + webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") + + Localization.assureCorrectAppLanguage(context) + AlertDialog.Builder(context) + .setTitle(license.name) + .setView(webView) + .block() + .show() + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index 1b8540808..255f5ba8d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.database; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; -import androidx.room.OnConflictStrategy; import androidx.room.Update; import java.util.Collection; @@ -14,13 +13,10 @@ import io.reactivex.rxjava3.core.Flowable; @Dao public interface BasicDAO { /* Inserts */ - @Insert(onConflict = OnConflictStrategy.ABORT) + @Insert long insert(Entity entity); - @Insert(onConflict = OnConflictStrategy.ABORT) - List insertAll(Entity... entities); - - @Insert(onConflict = OnConflictStrategy.ABORT) + @Insert List insertAll(Collection entities); /* Searches */ @@ -32,9 +28,6 @@ public interface BasicDAO { @Delete void delete(Entity entity); - @Delete - int delete(Collection entities); - int deleteAll(); /* Updates */ diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index d573788a6..b2b3d18a6 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -9,6 +9,7 @@ import androidx.room.Update import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Maybe import org.schabi.newpipe.database.feed.model.FeedEntity +import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.stream.StreamWithState import org.schabi.newpipe.database.stream.model.StreamStateEntity @@ -21,56 +22,16 @@ abstract class FeedDAO { @Query("DELETE FROM feed") abstract fun deleteAll(): Int - @Query( - """ - SELECT s.*, sst.progress_time - FROM streams s - - LEFT JOIN stream_state sst - ON s.uid = sst.stream_id - - LEFT JOIN stream_history sh - ON s.uid = sh.stream_id - - INNER JOIN feed f - ON s.uid = f.stream_id - - ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC - LIMIT 500 - """ - ) - abstract fun getAllStreams(): Maybe> - - @Query( - """ - SELECT s.*, sst.progress_time - FROM streams s - - LEFT JOIN stream_state sst - ON s.uid = sst.stream_id - - LEFT JOIN stream_history sh - ON s.uid = sh.stream_id - - INNER JOIN feed f - ON s.uid = f.stream_id - - INNER JOIN feed_group_subscription_join fgs - ON fgs.subscription_id = f.subscription_id - - WHERE fgs.group_id = :groupId - - ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC - LIMIT 500 - """ - ) - abstract fun getAllStreamsForGroup(groupId: Long): Maybe> - /** + * @param groupId the group id to get feed streams of; use + * [FeedGroupEntity.GROUP_ALL_ID] to not filter by group + * @param includePlayed if false, only return all of the live, never-played or non-finished + * feed streams (see `@see` items); if true no filter is applied + * @param uploadDateBefore get only streams uploaded before this date (useful to filter out + * future streams); use null to not filter by upload date + * @return the feed streams filtered according to the conditions provided in the parameters * @see StreamStateEntity.isFinished() * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS - * @return all of the non-live, never-played and non-finished streams in the feed - * (all of the cited conditions must hold for a stream to be in the returned list) */ @Query( """ @@ -79,67 +40,44 @@ abstract class FeedDAO { LEFT JOIN stream_state sst ON s.uid = sst.stream_id - + LEFT JOIN stream_history sh - ON s.uid = sh.stream_id - + ON s.uid = sh.stream_id + INNER JOIN feed f ON s.uid = f.stream_id + LEFT JOIN feed_group_subscription_join fgs + ON fgs.subscription_id = f.subscription_id + WHERE ( - sh.stream_id IS NULL - OR sst.stream_id IS NULL - OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS} - OR sst.progress_time < s.duration * 1000 * 3 / 4 - OR s.stream_type = 'LIVE_STREAM' - OR s.stream_type = 'AUDIO_LIVE_STREAM' + :groupId = ${FeedGroupEntity.GROUP_ALL_ID} + OR fgs.group_id = :groupId ) - - ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC - LIMIT 500 - """ - ) - abstract fun getLiveOrNotPlayedStreams(): Maybe> - - /** - * @see StreamStateEntity.isFinished() - * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS - * @param groupId the group id to get streams of - * @return all of the non-live, never-played and non-finished streams for the given feed group - * (all of the cited conditions must hold for a stream to be in the returned list) - */ - @Query( - """ - SELECT s.*, sst.progress_time - FROM streams s - - LEFT JOIN stream_state sst - ON s.uid = sst.stream_id - - LEFT JOIN stream_history sh - ON s.uid = sh.stream_id - - INNER JOIN feed f - ON s.uid = f.stream_id - - INNER JOIN feed_group_subscription_join fgs - ON fgs.subscription_id = f.subscription_id - - WHERE fgs.group_id = :groupId AND ( - sh.stream_id IS NULL + :includePlayed + OR sh.stream_id IS NULL OR sst.stream_id IS NULL OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS} OR sst.progress_time < s.duration * 1000 * 3 / 4 OR s.stream_type = 'LIVE_STREAM' OR s.stream_type = 'AUDIO_LIVE_STREAM' ) + AND ( + :uploadDateBefore IS NULL + OR s.upload_date IS NULL + OR s.upload_date < :uploadDateBefore + ) ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC LIMIT 500 """ ) - abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe> + abstract fun getStreams( + groupId: Long, + includePlayed: Boolean, + uploadDateBefore: OffsetDateTime? + ): Maybe> @Query( """ diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 43dbd89ea..695f9ec5a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -3,10 +3,10 @@ package org.schabi.newpipe.database.playlist; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; public interface PlaylistLocalItem extends LocalItem { String getOrderingName(); @@ -14,14 +14,9 @@ public interface PlaylistLocalItem extends LocalItem { static List merge( final List localPlaylists, final List remotePlaylists) { - final List items = new ArrayList<>( - localPlaylists.size() + remotePlaylists.size()); - items.addAll(localPlaylists); - items.addAll(remotePlaylists); - - Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName, - Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))); - - return items; + return Stream.concat(localPlaylists.stream(), remotePlaylists.stream()) + .sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName, + Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))) + .collect(Collectors.toList()); } } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index e4adddc2a..0e64e8b48 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -1,5 +1,9 @@ package org.schabi.newpipe.download; +import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP; +import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -82,10 +86,6 @@ import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.MissionState; -import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP; -import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; @@ -205,8 +205,8 @@ public class DownloadDialog extends DialogFragment setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); Icepick.restoreInstanceState(this, savedInstanceState); - final SparseArray> secondaryStreams - = new SparseArray<>(4); + final SparseArray> secondaryStreams = + new SparseArray<>(4); final List videoStreams = wrappedVideoStreams.getStreamsList(); for (int i = 0; i < videoStreams.size(); i++) { diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java index bd8430296..e1dd929d4 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; +import java.util.stream.Collectors; /* * Created by Christian Schabesberger on 24.10.15. @@ -65,11 +66,11 @@ public class ErrorActivity extends AppCompatActivity { public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; public static final String ERROR_EMAIL_SUBJECT = "Exception in "; - public static final String ERROR_GITHUB_ISSUE_URL - = "https://github.com/TeamNewPipe/NewPipe/issues"; + public static final 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"); + public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private ErrorInfo errorInfo; @@ -182,14 +183,9 @@ public class ErrorActivity extends AppCompatActivity { } private String formErrorText(final String[] el) { - final StringBuilder text = new StringBuilder(); - if (el != null) { - for (final String e : el) { - text.append("-------------------------------------\n").append(e); - } - } - text.append("-------------------------------------"); - return text.toString(); + final String separator = "-------------------------------------"; + return Arrays.stream(el) + .collect(Collectors.joining(separator + "\n", separator + "\n", separator)); } /** diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index f9f9f003a..d87fa3330 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -14,8 +14,6 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException import org.schabi.newpipe.ktx.isNetworkRelated import org.schabi.newpipe.util.ServiceHelper -import java.io.PrintWriter -import java.io.StringWriter @Parcelize class ErrorInfo( @@ -80,19 +78,10 @@ class ErrorInfo( companion object { const val SERVICE_NONE = "none" - private fun getStackTrace(throwable: Throwable): String { - StringWriter().use { stringWriter -> - PrintWriter(stringWriter, true).use { printWriter -> - throwable.printStackTrace(printWriter) - return stringWriter.buffer.toString() - } - } - } + fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString()) - fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable)) - - fun throwableListToStringList(throwable: List) = - Array(throwable.size) { i -> getStackTrace(throwable[i]) } + fun throwableListToStringList(throwableList: List) = + throwableList.map { it.stackTraceToString() }.toTypedArray() private fun getInfoServiceName(info: Info?) = if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId) diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt index e4dd2e16d..86e2e1028 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt @@ -114,13 +114,7 @@ class ErrorUtil { context, context.getString(R.string.error_report_channel_id) ) - .setSmallIcon( - // the vector drawable icon causes crashes on KitKat devices - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - R.drawable.ic_bug_report - else - android.R.drawable.stat_notify_error - ) + .setSmallIcon(R.drawable.ic_bug_report) .setContentTitle(context.getString(R.string.error_report_notification_title)) .setContentText(context.getString(errorInfo.messageStringId)) .setAutoCancel(true) diff --git a/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java index 555dd709b..e2780d215 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java @@ -3,14 +3,15 @@ package org.schabi.newpipe.error; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.webkit.CookieManager; +import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; +import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -18,7 +19,6 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NavUtils; import androidx.preference.PreferenceManager; -import androidx.webkit.WebViewClientCompat; import org.schabi.newpipe.databinding.ActivityRecaptchaBinding; import org.schabi.newpipe.DownloaderImpl; @@ -86,14 +86,15 @@ public class ReCaptchaActivity extends AppCompatActivity { webSettings.setJavaScriptEnabled(true); webSettings.setUserAgentString(DownloaderImpl.USER_AGENT); - recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() { + recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() { @Override - public boolean shouldOverrideUrlLoading(final WebView view, final String url) { + public boolean shouldOverrideUrlLoading(final WebView view, + final WebResourceRequest request) { if (MainActivity.DEBUG) { - Log.d(TAG, "shouldOverrideUrlLoading: url=" + url); + Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString()); } - handleCookiesFromUrl(url); + handleCookiesFromUrl(request.getUrl().toString()); return false; } @@ -107,12 +108,7 @@ public class ReCaptchaActivity extends AppCompatActivity { // cleaning cache, history and cookies from webView recaptchaBinding.reCaptchaWebView.clearCache(true); recaptchaBinding.reCaptchaWebView.clearHistory(); - final CookieManager cookieManager = CookieManager.getInstance(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies(value -> { }); - } else { - cookieManager.removeAllCookie(); - } + CookieManager.getInstance().removeAllCookies(null); recaptchaBinding.reCaptchaWebView.loadUrl(url); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index d57ddb02d..bf7f8fa5d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -1,5 +1,9 @@ package org.schabi.newpipe.fragments.detail; +import static android.text.TextUtils.isEmpty; +import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; + import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -26,17 +30,9 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.TextLinkifier; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import icepick.State; import io.reactivex.rxjava3.disposables.CompositeDisposable; -import static android.text.TextUtils.isEmpty; -import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; - public class DescriptionFragment extends BaseFragment { @State @@ -185,8 +181,8 @@ public class DescriptionFragment extends BaseFragment { return; } - final ItemMetadataBinding itemBinding - = ItemMetadataBinding.inflate(inflater, layout, false); + final ItemMetadataBinding itemBinding = + ItemMetadataBinding.inflate(inflater, layout, false); itemBinding.metadataTypeView.setText(type); itemBinding.metadataTypeView.setOnLongClickListener(v -> { @@ -206,19 +202,16 @@ public class DescriptionFragment extends BaseFragment { private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) { - final ItemMetadataTagsBinding itemBinding - = ItemMetadataTagsBinding.inflate(inflater, layout, false); + final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false); - final List tags = new ArrayList<>(streamInfo.getTags()); - Collections.sort(tags); - for (final String tag : tags) { + streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> { final Chip chip = (Chip) inflater.inflate(R.layout.chip, itemBinding.metadataTagsChips, false); chip.setText(tag); chip.setOnClickListener(this::onTagClick); chip.setOnLongClickListener(this::onTagLongClick); itemBinding.metadataTagsChips.addView(chip); - } + }); layout.addView(itemBinding.getRoot()); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 5e19f558d..09e085791 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1,5 +1,16 @@ package org.schabi.newpipe.fragments.detail; +import static android.text.TextUtils.isEmpty; +import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; +import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; +import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; +import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; +import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; +import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; +import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; + import android.animation.ValueAnimator; import android.app.Activity; import android.content.BroadcastReceiver; @@ -10,7 +21,6 @@ import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.database.ContentObserver; import android.graphics.Color; -import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -77,9 +87,9 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.PlayerService; +import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -87,6 +97,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.player.ui.MainPlayerUi; +import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; @@ -101,11 +113,11 @@ import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; import icepick.State; @@ -114,17 +126,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -import static android.text.TextUtils.isEmpty; -import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; -import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; -import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; -import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; -import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; -import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; -import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; - public final class VideoDetailFragment extends BaseStateFragment implements BackPressable, @@ -179,6 +180,8 @@ public final class VideoDetailFragment @State int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; @State + int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED; + @State protected boolean autoPlayEnabled = true; @Nullable @@ -190,6 +193,7 @@ public final class VideoDetailFragment private Disposable positionSubscriber = null; private BottomSheetBehavior bottomSheetBehavior; + private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback; private BroadcastReceiver broadcastReceiver; /*////////////////////////////////////////////////////////////////////////// @@ -202,7 +206,7 @@ public final class VideoDetailFragment private ContentObserver settingsContentObserver; @Nullable - private MainPlayer playerService; + private PlayerService playerService; private Player player; private final PlayerHolder playerHolder = PlayerHolder.getInstance(); @@ -211,7 +215,7 @@ public final class VideoDetailFragment //////////////////////////////////////////////////////////////////////////*/ @Override public void onServiceConnected(final Player connectedPlayer, - final MainPlayer connectedPlayerService, + final PlayerService connectedPlayerService, final boolean playAfterConnect) { player = connectedPlayer; playerService = connectedPlayerService; @@ -219,6 +223,7 @@ public final class VideoDetailFragment // It will do nothing if the player is not in fullscreen mode hideSystemUiIfNeeded(); + final Optional playerUi = player.UIs().get(MainPlayerUi.class); if (!player.videoPlayerSelected() && !playAfterConnect) { return; } @@ -227,22 +232,19 @@ public final class VideoDetailFragment // If the video is playing but orientation changed // let's make the video in fullscreen again checkLandscape(); - } else if (player.isFullscreen() && !player.isVerticalVideo() + } else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false) // Tablet UI has orientation-independent fullscreen && !DeviceUtils.isTablet(activity)) { // Device is in portrait orientation after rotation but UI is in fullscreen. // Return back to non-fullscreen state - player.toggleFullscreen(); - } - - if (playerIsNotStopped() && player.videoPlayerSelected()) { - addVideoPlayerView(); + playerUi.ifPresent(MainPlayerUi::toggleFullscreen); } + //noinspection SimplifyOptionalCallChains if (playAfterConnect || (currentInfo != null && isAutoplayEnabled() - && player.getParentActivity() == null)) { + && !playerUi.isPresent())) { autoPlayEnabled = true; // forcefully start playing openVideoPlayerAutoFullscreen(); } @@ -269,7 +271,7 @@ public final class VideoDetailFragment public static VideoDetailFragment getInstanceInCollapsedState() { final VideoDetailFragment instance = new VideoDetailFragment(); - instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED; + instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED); return instance; } @@ -329,6 +331,9 @@ public final class VideoDetailFragment @Override public void onResume() { super.onResume(); + if (DEBUG) { + Log.d(TAG, "onResume() called"); + } activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); @@ -383,7 +388,7 @@ public final class VideoDetailFragment disposables.clear(); positionSubscriber = null; currentWorker = null; - bottomSheetBehavior.setBottomSheetCallback(null); + bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback); if (activity.isFinishing()) { playQueue = null; @@ -449,7 +454,7 @@ public final class VideoDetailFragment disposables.add( PlaylistDialog.createCorrespondingDialog( getContext(), - Collections.singletonList(new StreamEntity(currentInfo)), + List.of(new StreamEntity(currentInfo)), dialog -> dialog.show(getFM(), TAG) ) ); @@ -500,12 +505,18 @@ public final class VideoDetailFragment } break; case R.id.detail_thumbnail_root_layout: - autoPlayEnabled = true; // forcefully start playing - // FIXME Workaround #7427 - if (isPlayerAvailable()) { - player.setRecovery(); + // make sure not to open any player if there is nothing currently loaded! + // FIXME removing this `if` causes the player service to start correctly, then stop, + // then restart badly without calling `startForeground()`, causing a crash when + // later closing the detail fragment + if (currentInfo != null) { + autoPlayEnabled = true; // forcefully start playing + // FIXME Workaround #7427 + if (isPlayerAvailable()) { + player.setRecovery(); + } + openVideoPlayerAutoFullscreen(); } - openVideoPlayerAutoFullscreen(); break; case R.id.detail_title_root_layout: toggleTitleAndSecondaryControls(); @@ -518,7 +529,7 @@ public final class VideoDetailFragment case R.id.overlay_play_pause_button: if (playerIsNotStopped()) { player.playPause(); - player.hideControls(0, 0); + player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); showSystemUi(); } else { autoPlayEnabled = true; // forcefully start playing @@ -583,12 +594,12 @@ public final class VideoDetailFragment if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) { binding.detailVideoTitleView.setMaxLines(10); animateRotation(binding.detailToggleSecondaryControlsView, - Player.DEFAULT_CONTROLS_DURATION, 180); + VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180); binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE); } else { binding.detailVideoTitleView.setMaxLines(1); animateRotation(binding.detailToggleSecondaryControlsView, - Player.DEFAULT_CONTROLS_DURATION, 0); + VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0); binding.detailSecondaryControlPanel.setVisibility(View.GONE); } // view pager height has changed, update the tab layout @@ -714,7 +725,7 @@ public final class VideoDetailFragment } private void initThumbnailViews(@NonNull final StreamInfo info) { - PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG) + PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG) .into(binding.detailThumbnailImageView, new Callback() { @Override public void onSuccess() { @@ -746,7 +757,9 @@ public final class VideoDetailFragment @Override public boolean onKeyDown(final int keyCode) { - return isPlayerAvailable() && player.onKeyDown(keyCode); + return isPlayerAvailable() + && player.UIs().get(VideoPlayerUi.class) + .map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false); } @Override @@ -756,7 +769,7 @@ public final class VideoDetailFragment } // If we are in fullscreen mode just exit from it via first back press - if (isPlayerAvailable() && player.isFullscreen()) { + if (isFullscreen()) { if (!DeviceUtils.isTablet(activity)) { player.pause(); } @@ -1006,8 +1019,7 @@ public final class VideoDetailFragment getChildFragmentManager().beginTransaction() .replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info)) .commitAllowingStateLoss(); - binding.relatedItemsLayout.setVisibility( - isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE); + binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE); } } @@ -1047,15 +1059,13 @@ public final class VideoDetailFragment // call `post()` to be sure `viewPager.getHitRect()` // is up to date and not being currently recomputed binding.tabLayout.post(() -> { - if (getContext() != null) { + final var activity = getActivity(); + if (activity != null) { final Rect pagerHitRect = new Rect(); binding.viewPager.getHitRect(pagerHitRect); - final Point displaySize = new Point(); - Objects.requireNonNull(ContextCompat.getSystemService(getContext(), - WindowManager.class)).getDefaultDisplay().getSize(displaySize); - - final int viewPagerVisibleHeight = displaySize.y - pagerHitRect.top; + final int height = DeviceUtils.getWindowHeight(activity.getWindowManager()); + final int viewPagerVisibleHeight = height - pagerHitRect.top; // see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp final float tabLayoutHeight = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()); @@ -1087,8 +1097,12 @@ public final class VideoDetailFragment private void toggleFullscreenIfInFullscreenMode() { // If a user watched video inside fullscreen mode and than chose another player // return to non-fullscreen mode - if (isPlayerAvailable() && player.isFullscreen()) { - player.toggleFullscreen(); + if (isPlayerAvailable()) { + player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { + if (playerUi.isFullscreen()) { + playerUi.toggleFullscreen(); + } + }); } } @@ -1164,7 +1178,7 @@ public final class VideoDetailFragment // doesn't tell which state it was settling to, and thus the bottom sheet settles to // STATE_COLLAPSED. This can be solved by manually setting the state that will be // restored (i.e. bottomSheetState) to STATE_EXPANDED. - bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; + updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED); // toggle landscape in order to open directly in fullscreen onScreenRotationButtonClicked(); } @@ -1214,16 +1228,10 @@ public final class VideoDetailFragment } final PlayQueue queue = setupPlayQueueForIntent(false); - - // Video view can have elements visible from popup, - // We hide it here but once it ready the view will be shown in handleIntent() - if (playerService.getView() != null) { - playerService.getView().setVisibility(View.GONE); - } - addVideoPlayerView(); + tryAddVideoPlayerView(); final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), - MainPlayer.class, queue, true, autoPlayEnabled); + PlayerService.class, queue, true, autoPlayEnabled); ContextCompat.startForegroundService(activity, playerIntent); } @@ -1235,8 +1243,8 @@ public final class VideoDetailFragment * be reused in a few milliseconds and the flickering would be annoying. */ private void hideMainPlayerOnLoadingNewStream() { - if (!isPlayerServiceAvailable() - || playerService.getView() == null + //noinspection SimplifyOptionalCallChains + if (!isPlayerServiceAvailable() || !getRoot().isPresent() || !player.videoPlayerSelected()) { return; } @@ -1244,7 +1252,7 @@ public final class VideoDetailFragment removeVideoPlayerView(); if (isAutoplayEnabled()) { playerService.stopForImmediateReusing(); - playerService.getView().setVisibility(View.GONE); + getRoot().ifPresent(view -> view.setVisibility(View.GONE)); } else { playerHolder.stopService(); } @@ -1301,27 +1309,41 @@ public final class VideoDetailFragment && PlayerHelper.isAutoplayAllowedByUser(requireContext()); } - private void addVideoPlayerView() { - if (!isPlayerAvailable() || getView() == null) { - return; + private void tryAddVideoPlayerView() { + if (isPlayerAvailable() && getView() != null) { + // Setup the surface view height, so that it fits the video correctly; this is done also + // here, and not only in the Handler, to avoid a choppy fullscreen rotation animation. + setHeightThumbnail(); } - // Check if viewHolder already contains a child - if (player.getRootView().getParent() != binding.playerPlaceholder) { - playerService.removeViewFromParent(); - } - setHeightThumbnail(); + // do all the null checks in the posted lambda, too, since the player, the binding and the + // view could be set or unset before the lambda gets executed on the next main thread cycle + new Handler(Looper.getMainLooper()).post(() -> { + if (!isPlayerAvailable() || getView() == null) { + return; + } - // Prevent from re-adding a view multiple times - if (player.getRootView().getParent() == null) { - binding.playerPlaceholder.addView(player.getRootView()); - } + // setup the surface view height, so that it fits the video correctly + setHeightThumbnail(); + + player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { + // sometimes binding would be null here, even though getView() != null above u.u + if (binding != null) { + // prevent from re-adding a view multiple times + playerUi.removeViewFromParent(); + binding.playerPlaceholder.addView(playerUi.getBinding().getRoot()); + playerUi.setupVideoSurfaceIfNeeded(); + } + }); + }); } private void removeVideoPlayerView() { makeDefaultHeightForVideoPlaceholder(); - playerService.removeViewFromParent(); + if (player != null) { + player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent); + } } private void makeDefaultHeightForVideoPlaceholder() { @@ -1362,7 +1384,7 @@ public final class VideoDetailFragment final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); - if (isPlayerAvailable() && player.isFullscreen()) { + if (isFullscreen()) { final int height = (DeviceUtils.isInMultiWindow(activity) ? requireView() : activity.getWindow().getDecorView()).getHeight(); @@ -1387,8 +1409,9 @@ public final class VideoDetailFragment binding.detailThumbnailImageView.setMinimumHeight(newHeight); if (isPlayerAvailable()) { final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); - player.getSurfaceView() - .setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight); + player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> + ui.getBinding().surfaceView.setHeights(newHeight, + ui.isFullscreen() ? newHeight : maxHeight)); } } @@ -1517,7 +1540,7 @@ public final class VideoDetailFragment if (binding.relatedItemsLayout != null) { if (showRelatedItems) { binding.relatedItemsLayout.setVisibility( - isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE); + isFullscreen() ? View.GONE : View.INVISIBLE); } else { binding.relatedItemsLayout.setVisibility(View.GONE); } @@ -1551,7 +1574,8 @@ public final class VideoDetailFragment binding.detailUploaderThumbnailView.setVisibility(View.GONE); } - final Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy); + final Drawable buddyDrawable = + AppCompatResources.getDrawable(activity, R.drawable.placeholder_person); binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable); binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable); @@ -1778,6 +1802,11 @@ public final class VideoDetailFragment // Player event listener //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onViewCreated() { + tryAddVideoPlayerView(); + } + @Override public void onQueueUpdate(final PlayQueue queue) { playQueue = queue; @@ -1898,15 +1927,10 @@ public final class VideoDetailFragment @Override public void onFullscreenStateChanged(final boolean fullscreen) { setupBrightness(); + //noinspection SimplifyOptionalCallChains if (!isPlayerAndPlayerServiceAvailable() - || playerService.getView() == null - || player.getParentActivity() == null) { - return; - } - - final View view = playerService.getView(); - final ViewGroup parent = (ViewGroup) view.getParent(); - if (parent == null) { + || !player.UIs().get(MainPlayerUi.class).isPresent() + || getRoot().map(View::getParent).orElse(null) == null) { return; } @@ -1922,13 +1946,7 @@ public final class VideoDetailFragment } scrollToTop(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - addVideoPlayerView(); - } else { - // KitKat needs a delay before addVideoPlayerView call or it reports wrong height in - // activity.getWindow().getDecorView().getHeight() - new Handler().post(this::addVideoPlayerView); - } + tryAddVideoPlayerView(); } @Override @@ -1940,7 +1958,7 @@ public final class VideoDetailFragment final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); if (DeviceUtils.isTablet(activity) && (!globalScreenOrientationLocked(activity) || isLandscape)) { - player.toggleFullscreen(); + player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen); return; } @@ -1991,10 +2009,8 @@ public final class VideoDetailFragment } activity.getWindow().getDecorView().setSystemUiVisibility(0); activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr( - requireContext(), android.R.attr.colorPrimary)); - } + activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr( + requireContext(), android.R.attr.colorPrimary)); } private void hideSystemUi() { @@ -2025,8 +2041,7 @@ public final class VideoDetailFragment } activity.getWindow().getDecorView().setSystemUiVisibility(visibility); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) { + if (isInMultiWindow || isFullscreen()) { activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); } @@ -2034,14 +2049,19 @@ public final class VideoDetailFragment } // Listener implementation + @Override public void hideSystemUiIfNeeded() { - if (isPlayerAvailable() - && player.isFullscreen() + if (isFullscreen() && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { hideSystemUi(); } } + private boolean isFullscreen() { + return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class) + .map(VideoPlayerUi::isFullscreen).orElse(false); + } + private boolean playerIsNotStopped() { return isPlayerAvailable() && !player.isStopped(); } @@ -2064,10 +2084,7 @@ public final class VideoDetailFragment } final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); - if (!isPlayerAvailable() - || !player.videoPlayerSelected() - || !player.isFullscreen() - || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) { + if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) { // Apply system brightness when the player is not in fullscreen restoreDefaultBrightness(); } else { @@ -2091,7 +2108,7 @@ public final class VideoDetailFragment setAutoPlay(true); } - player.checkLandscape(); + player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape); // Let's give a user time to look at video information page if video is not playing if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { player.play(); @@ -2170,12 +2187,8 @@ public final class VideoDetailFragment } else { final int selectedVideoStreamIndexForExternalPlayers = ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers); - final CharSequence[] resolutions = - new CharSequence[videoStreamsForExternalPlayers.size()]; - - for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) { - resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution(); - } + final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream() + .map(VideoStream::getResolution).toArray(CharSequence[]::new); builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers, null); @@ -2279,7 +2292,9 @@ public final class VideoDetailFragment final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder); bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); - bottomSheetBehavior.setState(bottomSheetState); + bottomSheetBehavior.setState(lastStableBottomSheetState); + updateBottomSheetState(lastStableBottomSheetState); + final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height); if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) { manageSpaceAtTheBottom(false); @@ -2292,10 +2307,10 @@ public final class VideoDetailFragment } } - bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull final View bottomSheet, final int newState) { - bottomSheetState = newState; + updateBottomSheetState(newState); switch (newState) { case BottomSheetBehavior.STATE_HIDDEN: @@ -2318,10 +2333,10 @@ public final class VideoDetailFragment if (DeviceUtils.isLandscape(requireContext()) && isPlayerAvailable() && player.isPlaying() - && !player.isFullscreen() - && !DeviceUtils.isTablet(activity) - && player.videoPlayerSelected()) { - player.toggleFullscreen(); + && !isFullscreen() + && !DeviceUtils.isTablet(activity)) { + player.UIs().get(MainPlayerUi.class) + .ifPresent(MainPlayerUi::toggleFullscreen); } setOverlayLook(binding.appBarLayout, behavior, 1); break; @@ -2334,19 +2349,26 @@ public final class VideoDetailFragment // Re-enable clicks setOverlayElementsClickable(true); if (isPlayerAvailable()) { - player.closeItemsList(); + player.UIs().get(MainPlayerUi.class) + .ifPresent(MainPlayerUi::closeItemsList); } setOverlayLook(binding.appBarLayout, behavior, 0); break; case BottomSheetBehavior.STATE_DRAGGING: case BottomSheetBehavior.STATE_SETTLING: - if (isPlayerAvailable() && player.isFullscreen()) { + if (isFullscreen()) { showSystemUi(); } - if (isPlayerAvailable() && player.isControlsVisible()) { - player.hideControls(0, 0); + if (isPlayerAvailable()) { + player.UIs().get(MainPlayerUi.class).ifPresent(ui -> { + if (ui.isControlsVisible()) { + ui.hideControls(0, 0); + } + }); } break; + case BottomSheetBehavior.STATE_HALF_EXPANDED: + break; } } @@ -2354,7 +2376,9 @@ public final class VideoDetailFragment public void onSlide(@NonNull final View bottomSheet, final float slideOffset) { setOverlayLook(binding.appBarLayout, behavior, slideOffset); } - }); + }; + + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback); // User opened a new page and the player will hide itself activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> { @@ -2369,8 +2393,8 @@ public final class VideoDetailFragment @Nullable final String thumbnailUrl) { binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle); binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader); - binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark); - PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG) + binding.overlayThumbnail.setImageDrawable(null); + PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG) .into(binding.overlayThumbnail); } @@ -2418,4 +2442,21 @@ public final class VideoDetailFragment boolean isPlayerAndPlayerServiceAvailable() { return (player != null && playerService != null); } + + public Optional getRoot() { + if (player == null) { + return Optional.empty(); + } + + return player.UIs().get(VideoPlayerUi.class) + .map(playerUi -> playerUi.getBinding().getRoot()); + } + + private void updateBottomSheetState(final int newState) { + bottomSheetState = newState; + if (newState != BottomSheetBehavior.STATE_DRAGGING + && newState != BottomSheetBehavior.STATE_SETTLING) { + lastStableBottomSheetState = newState; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java index 55336a42f..c816723ff 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java @@ -6,6 +6,7 @@ import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECI import android.content.Context; import android.util.Log; +import android.util.Pair; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -28,9 +29,7 @@ import org.schabi.newpipe.player.Player; import org.schabi.newpipe.util.ThemeHelper; import java.io.IOException; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.List; import java.util.function.Supplier; /** @@ -43,50 +42,34 @@ public final class VideoDetailPlayerCrasher { // https://stackoverflow.com/a/54744028 private static final String TAG = "VideoDetPlayerCrasher"; - private static final Map> AVAILABLE_EXCEPTION_TYPES = - getExceptionTypes(); + private static final String DEFAULT_MSG = "Dummy"; + + private static final List>> + AVAILABLE_EXCEPTION_TYPES = List.of( + new Pair<>("Source", () -> ExoPlaybackException.createForSource( + new IOException(DEFAULT_MSG), + ERROR_CODE_BEHIND_LIVE_WINDOW + )), + new Pair<>("Renderer", () -> ExoPlaybackException.createForRenderer( + new Exception(DEFAULT_MSG), + "Dummy renderer", + 0, + null, + C.FORMAT_HANDLED, + /*isRecoverable=*/false, + ERROR_CODE_DECODING_FAILED + )), + new Pair<>("Unexpected", () -> ExoPlaybackException.createForUnexpected( + new RuntimeException(DEFAULT_MSG), + ERROR_CODE_UNSPECIFIED + )), + new Pair<>("Remote", () -> ExoPlaybackException.createForRemote(DEFAULT_MSG)) + ); private VideoDetailPlayerCrasher() { // No impls } - private static Map> getExceptionTypes() { - final String defaultMsg = "Dummy"; - final Map> exceptionTypes = new LinkedHashMap<>(); - exceptionTypes.put( - "Source", - () -> ExoPlaybackException.createForSource( - new IOException(defaultMsg), - ERROR_CODE_BEHIND_LIVE_WINDOW - ) - ); - exceptionTypes.put( - "Renderer", - () -> ExoPlaybackException.createForRenderer( - new Exception(defaultMsg), - "Dummy renderer", - 0, - null, - C.FORMAT_HANDLED, - /*isRecoverable=*/false, - ERROR_CODE_DECODING_FAILED - ) - ); - exceptionTypes.put( - "Unexpected", - () -> ExoPlaybackException.createForUnexpected( - new RuntimeException(defaultMsg), - ERROR_CODE_UNSPECIFIED - ) - ); - exceptionTypes.put( - "Remote", - () -> ExoPlaybackException.createForRemote(defaultMsg) - ); - - return Collections.unmodifiableMap(exceptionTypes); - } - private static Context getThemeWrapperContext(final Context context) { return new ContextThemeWrapper( context, @@ -121,10 +104,9 @@ public final class VideoDetailPlayerCrasher { .setNegativeButton(R.string.cancel, null) .create(); - for (final Map.Entry> entry - : AVAILABLE_EXCEPTION_TYPES.entrySet()) { + for (final Pair> entry : AVAILABLE_EXCEPTION_TYPES) { final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot(); - radioButton.setText(entry.getKey()); + radioButton.setText(entry.first); radioButton.setChecked(false); radioButton.setLayoutParams( new RadioGroup.LayoutParams( @@ -133,7 +115,7 @@ public final class VideoDetailPlayerCrasher { ) ); radioButton.setOnClickListener(v -> { - tryCrashPlayerWith(player, entry.getValue().get()); + tryCrashPlayerWith(player, entry.second.get()); alertDialog.cancel(); }); binding.list.addView(radioButton); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 27e5a8571..9e7cb757c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -23,14 +23,11 @@ import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.extractor.comments.CommentsInfoItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; @@ -264,45 +261,28 @@ public abstract class BaseListFragment extends BaseStateFragment } }); - infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() { - @Override - public void selected(final ChannelInfoItem selectedItem) { - try { - onItemSelected(selectedItem); - NavigationHelper.openChannelFragment(getFM(), - selectedItem.getServiceId(), - selectedItem.getUrl(), - selectedItem.getName()); - } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar( - BaseListFragment.this, "Opening channel fragment", e); - } - } - }); - - infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() { - @Override - public void selected(final PlaylistInfoItem selectedItem) { - try { - onItemSelected(selectedItem); - NavigationHelper.openPlaylistFragment(getFM(), - selectedItem.getServiceId(), - selectedItem.getUrl(), - selectedItem.getName()); - } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar(BaseListFragment.this, - "Opening playlist fragment", e); - } - } - }); - - infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() { - @Override - public void selected(final CommentsInfoItem selectedItem) { + infoListAdapter.setOnChannelSelectedListener(selectedItem -> { + try { onItemSelected(selectedItem); + NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(), + selectedItem.getUrl(), selectedItem.getName()); + } catch (final Exception e) { + ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e); } }); + infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> { + try { + onItemSelected(selectedItem); + NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(), + selectedItem.getUrl(), selectedItem.getName()); + } catch (final Exception e) { + ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e); + } + }); + + infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected); + // Ensure that there is always a scroll listener (e.g. when rotating the device) useNormalItemListScrollListener(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index fa8f5fdbd..8ed9389c3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.feed.notifications.NotificationHelper; -import org.schabi.newpipe.player.MainPlayer.PlayerType; +import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.util.ExtractorHelper; @@ -578,17 +578,13 @@ public class ChannelFragment extends BaseListInfoFragment streamItems = infoListAdapter.getItemsList().stream() .filter(StreamInfoItem.class::isInstance) .map(StreamInfoItem.class::cast) .collect(Collectors.toList()); return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(), - currentInfo.getNextPage(), streamItems, index); + currentInfo.getNextPage(), streamItems, 0); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index ed63c6fd7..e3caeb522 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; -import org.schabi.newpipe.player.MainPlayer.PlayerType; +import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.util.ExtractorHelper; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 055c27733..5175e0096 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -200,7 +200,7 @@ public class SearchFragment extends BaseListFragment()); + suggestionListAdapter.submitList(null); showKeyboardSearch(); }); @@ -922,7 +921,7 @@ public class SearchFragment extends BaseListFragment suggestionListAdapter.setItems(suggestions)); + suggestionListAdapter.submitList(suggestions, + () -> searchBinding.suggestionsList.scrollToPosition(0)); if (suggestionsPanelVisible && isErrorPanelVisible()) { hideLoading(); @@ -983,8 +982,7 @@ public class SearchFragment extends BaseListFragment cannot be bundled without creating some containers - metaInfo = new MetaInfo[result.getMetaInfo().size()]; - metaInfo = result.getMetaInfo().toArray(metaInfo); + metaInfo = result.getMetaInfo().toArray(new MetaInfo[0]); showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator, disposables); @@ -1070,14 +1068,14 @@ public class SearchFragment extends BaseListFragment { - private final ArrayList items = new ArrayList<>(); - private final Context context; + extends ListAdapter { private OnSuggestionItemSelected listener; - public SuggestionListAdapter(final Context context) { - this.context = context; - } - - public void setItems(final List items) { - this.items.clear(); - this.items.addAll(items); - notifyDataSetChanged(); + public SuggestionListAdapter() { + super(new SuggestionItemCallback()); } public void setListener(final OnSuggestionItemSelected listener) { @@ -39,45 +27,32 @@ public class SuggestionListAdapter @Override public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - return new SuggestionItemHolder(LayoutInflater.from(context) - .inflate(R.layout.item_search_suggestion, parent, false)); + return new SuggestionItemHolder(ItemSearchSuggestionBinding + .inflate(LayoutInflater.from(parent.getContext()), parent, false)); } @Override public void onBindViewHolder(final SuggestionItemHolder holder, final int position) { final SuggestionItem currentItem = getItem(position); holder.updateFrom(currentItem); - holder.queryView.setOnClickListener(v -> { + holder.itemBinding.suggestionSearch.setOnClickListener(v -> { if (listener != null) { listener.onSuggestionItemSelected(currentItem); } }); - holder.queryView.setOnLongClickListener(v -> { + holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> { if (listener != null) { listener.onSuggestionItemLongClick(currentItem); } return true; }); - holder.insertView.setOnClickListener(v -> { + holder.itemBinding.suggestionInsert.setOnClickListener(v -> { if (listener != null) { listener.onSuggestionItemInserted(currentItem); } }); } - SuggestionItem getItem(final int position) { - return items.get(position); - } - - @Override - public int getItemCount() { - return items.size(); - } - - public boolean isEmpty() { - return getItemCount() == 0; - } - public interface OnSuggestionItemSelected { void onSuggestionItemSelected(SuggestionItem item); @@ -87,30 +62,32 @@ public class SuggestionListAdapter } public static final class SuggestionItemHolder extends RecyclerView.ViewHolder { - private final TextView itemSuggestionQuery; - private final ImageView suggestionIcon; - private final View queryView; - private final View insertView; + private final ItemSearchSuggestionBinding itemBinding; - // Cache some ids, as they can potentially be constantly updated/recycled - private final int historyResId; - private final int searchResId; - - private SuggestionItemHolder(final View rootView) { - super(rootView); - suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon); - itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query); - - queryView = rootView.findViewById(R.id.suggestion_search); - insertView = rootView.findViewById(R.id.suggestion_insert); - - historyResId = R.drawable.ic_history; - searchResId = R.drawable.ic_search; + private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) { + super(binding.getRoot()); + this.itemBinding = binding; } private void updateFrom(final SuggestionItem item) { - suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId); - itemSuggestionQuery.setText(item.query); + itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history + : R.drawable.ic_search); + itemBinding.itemSuggestionQuery.setText(item.query); + } + } + + private static class SuggestionItemCallback extends DiffUtil.ItemCallback { + @Override + public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem, + @NonNull final SuggestionItem newItem) { + return oldItem.fromHistory == newItem.fromHistory + && oldItem.query.equals(newItem.query); + } + + @Override + public boolean areContentsTheSame(@NonNull final SuggestionItem oldItem, + @NonNull final SuggestionItem newItem) { + return true; // items' contents never change; the list of items themselves does } } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index d78bf1076..68f19ee97 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -67,8 +67,8 @@ public class InfoItemBuilder { public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager, final boolean useMiniVariant) { - final InfoItemHolder holder - = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); + final InfoItemHolder holder = + holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); holder.updateFromItem(infoItem, historyRecordManager); return holder.itemView; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java index 5afaea038..61a88bb8f 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -321,6 +321,7 @@ public final class InfoItemDialog { */ public Builder addDefaultEndEntries() { addAllEntries( + StreamDialogDefaultEntry.DOWNLOAD, StreamDialogDefaultEntry.APPEND_PLAYLIST, StreamDialogDefaultEntry.SHARE, StreamDialogDefaultEntry.OPEN_IN_BROWSER diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java index 7e87318ee..1265e9767 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list.dialog; import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment; import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse; +import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase; import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse; import android.net.Uri; @@ -11,6 +12,7 @@ import androidx.annotation.StringRes; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -18,7 +20,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; -import java.util.Collections; +import java.util.List; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -87,7 +89,7 @@ public enum StreamDialogDefaultEntry { APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) -> PlaylistDialog.createCorrespondingDialog( fragment.getContext(), - Collections.singletonList(new StreamEntity(item)), + List.of(new StreamEntity(item)), dialog -> dialog.show( fragment.getParentFragmentManager(), "StreamDialogEntry@" @@ -110,6 +112,15 @@ public enum StreamDialogDefaultEntry { ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), item.getThumbnailUrl())), + DOWNLOAD(R.string.download, (fragment, item) -> + fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(), + item.getUrl(), info -> { + final DownloadDialog downloadDialog = + new DownloadDialog(fragment.requireContext(), info); + downloadDialog.show(fragment.getChildFragmentManager(), "downloadDialog"); + }) + ), + OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) -> ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index aa4f4c9f0..89398a1e5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -42,7 +42,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { itemTitleView.setText(item.getName()); itemAdditionalDetailView.setText(getDetailLine(item)); - PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); + PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView); itemView.setOnClickListener(view -> { if (itemBuilder.getOnChannelSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 6e4773c09..b900750a8 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -23,9 +23,9 @@ import org.schabi.newpipe.util.CommentTextOnTouchListener; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.external_communication.TimestampExtractor; -import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.external_communication.TimestampExtractor; import java.util.regex.Matcher; @@ -204,8 +204,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { boolean hasEllipsis = false; if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { - final int endOfLastLine - = itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1); + final int endOfLastLine = itemContentView + .getLayout() + .getLineEnd(COMMENT_DEFAULT_LINES - 1); int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2); if (end == -1) { end = Math.max(endOfLastLine - 2, 0); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 54d31ca57..8d17017d2 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -14,8 +14,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.views.AnimatedProgressBar; @@ -111,8 +111,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { final HistoryRecordManager historyRecordManager) { final StreamInfoItem item = (StreamInfoItem) infoItem; - final StreamStateEntity state - = historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; + final StreamStateEntity state = historyRecordManager + .loadStreamState(infoItem) + .blockingGet()[0]; if (state != null && item.getDuration() > 0 && !StreamTypeUtil.isLiveStream(item.getStreamType())) { itemProgressView.setMax((int) item.getDuration()); diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt index ace1dbf7e..bf0dcb201 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt @@ -21,10 +21,6 @@ import org.schabi.newpipe.MainActivity private const val TAG = "ViewUtils" -inline var View.backgroundTintListCompat: ColorStateList? - get() = ViewCompat.getBackgroundTintList(this) - set(value) = ViewCompat.setBackgroundTintList(this, value) - /** * Animate the view. * @@ -96,62 +92,43 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo if (MainActivity.DEBUG) { Log.d( TAG, - "animateBackgroundColor() called with: " + - "view = [" + this + "], duration = [" + duration + "], " + - "colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]" + "animateBackgroundColor() called with: view = [$this], duration = [$duration], " + + "colorStart = [$colorStart], colorEnd = [$colorEnd]" ) } - val empty = arrayOf(IntArray(0)) val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd) viewPropertyAnimator.interpolator = FastOutSlowInInterpolator() viewPropertyAnimator.duration = duration - viewPropertyAnimator.addUpdateListener { animation: ValueAnimator -> - backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int)) + + fun listenerAction(color: Int) { + ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color)) } - viewPropertyAnimator.addListener( - onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }, - onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) } - ) + viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) } + viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) }) viewPropertyAnimator.start() } fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator { if (MainActivity.DEBUG) { - Log.d( - TAG, - "animateHeight: duration = [" + duration + "], " + - "from " + height + " to → " + targetHeight + " in: " + this - ) + Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this") } val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat()) animator.interpolator = FastOutSlowInInterpolator() animator.duration = duration - animator.addUpdateListener { animation: ValueAnimator -> - val value = animation.animatedValue as Float - layoutParams.height = value.toInt() + + fun listenerAction(value: Int) { + layoutParams.height = value requestLayout() } - animator.addListener( - onCancel = { - layoutParams.height = targetHeight - requestLayout() - }, - onEnd = { - layoutParams.height = targetHeight - requestLayout() - } - ) + animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) } + animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) }) animator.start() return animator } fun View.animateRotation(duration: Long, targetRotation: Int) { if (MainActivity.DEBUG) { - Log.d( - TAG, - "animateRotation: duration = [" + duration + "], " + - "from " + rotation + " to → " + targetRotation + " in: " + this - ) + Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this") } animate().setListener(null).cancel() animate() @@ -172,20 +149,13 @@ private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long, if (enterOrExit) { animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - execOnEnd?.run() - } - }).start() + .setListener(ExecOnEndListener(execOnEnd)) + .start() } else { animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - isGone = true - execOnEnd?.run() - } - }).start() + .setListener(HideAndExecOnEndListener(this, execOnEnd)) + .start() } } @@ -197,11 +167,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela .setInterpolator(FastOutSlowInInterpolator()) .alpha(1f).scaleX(1f).scaleY(1f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - execOnEnd?.run() - } - }).start() + .setListener(ExecOnEndListener(execOnEnd)) + .start() } else { scaleX = 1f scaleY = 1f @@ -209,12 +176,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela .setInterpolator(FastOutSlowInInterpolator()) .alpha(0f).scaleX(.8f).scaleY(.8f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - isGone = true - execOnEnd?.run() - } - }).start() + .setListener(HideAndExecOnEndListener(this, execOnEnd)) + .start() } } @@ -227,11 +190,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long, .setInterpolator(FastOutSlowInInterpolator()) .alpha(1f).scaleX(1f).scaleY(1f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - execOnEnd?.run() - } - }).start() + .setListener(ExecOnEndListener(execOnEnd)) + .start() } else { alpha = 1f scaleX = 1f @@ -240,12 +200,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long, .setInterpolator(FastOutSlowInInterpolator()) .alpha(0f).scaleX(.95f).scaleY(.95f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - isGone = true - execOnEnd?.run() - } - }).start() + .setListener(HideAndExecOnEndListener(this, execOnEnd)) + .start() } } @@ -256,22 +212,15 @@ private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, dela animate() .setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - execOnEnd?.run() - } - }).start() + .setListener(ExecOnEndListener(execOnEnd)) + .start() } else { animate() .setInterpolator(FastOutSlowInInterpolator()) .alpha(0f).translationY(-height.toFloat()) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - isGone = true - execOnEnd?.run() - } - }).start() + .setListener(HideAndExecOnEndListener(this, execOnEnd)) + .start() } } @@ -282,21 +231,14 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long, animate() .setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - execOnEnd?.run() - } - }).start() + .setListener(ExecOnEndListener(execOnEnd)) + .start() } else { animate().setInterpolator(FastOutSlowInInterpolator()) .alpha(0f).translationY(-height / 2.0f) .setDuration(duration).setStartDelay(delay) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - isGone = true - execOnEnd?.run() - } - }).start() + .setListener(HideAndExecOnEndListener(this, execOnEnd)) + .start() } } @@ -318,11 +260,7 @@ fun View.slideUp( .setStartDelay(delay) .setDuration(duration) .setInterpolator(FastOutSlowInInterpolator()) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - execOnEnd?.run() - } - }) + .setListener(ExecOnEndListener(execOnEnd)) .start() } @@ -336,6 +274,20 @@ fun View.animateHideRecyclerViewAllowingScrolling() { animate().alpha(0.0f).setDuration(200).start() } +private open class ExecOnEndListener(private val execOnEnd: Runnable?) : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + execOnEnd?.run() + } +} + +private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnable?) : + ExecOnEndListener(execOnEnd) { + override fun onAnimationEnd(animation: Animator) { + view.isGone = true + super.onAnimationEnd(animation) + } +} + enum class AnimationType { ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA } diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index f272a8831..be7414542 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -98,7 +98,7 @@ public final class BookmarkFragment extends BaseLocalListFragment() { + itemListAdapter.setSelectedListener(new OnClickGesture<>() { @Override public void selected(final LocalItem selectedItem) { final FragmentManager fragmentManager = getFM(); @@ -256,8 +256,8 @@ public final class BookmarkFragment extends BaseLocalListFragment() { - @Override - public void selected(final LocalItem selectedItem) { - if (!(selectedItem instanceof PlaylistMetadataEntry) - || getStreamEntities() == null) { - return; - } - onPlaylistSelected( - playlistManager, - (PlaylistMetadataEntry) selectedItem, - getStreamEntities() - ); + playlistAdapter.setSelectedListener(selectedItem -> { + final List entities = getStreamEntities(); + if (selectedItem instanceof PlaylistMetadataEntry && entities != null) { + onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities); } }); @@ -138,14 +128,11 @@ public final class PlaylistAppendDialog extends PlaylistDialog { private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, @NonNull final PlaylistMetadataEntry playlist, @NonNull final List streams) { - if (getStreamEntities() == null) { - return; - } - final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); - if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) { + if (playlist.thumbnailUrl + .equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) { playlistDisposables.add(manager .changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java index 0c09f3f0d..0d5cfac23 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java @@ -45,8 +45,8 @@ public final class PlaylistCreationDialog extends PlaylistDialog { return super.onCreateDialog(savedInstanceState); } - final DialogEditTextBinding dialogBinding - = DialogEditTextBinding.inflate(getLayoutInflater()); + final DialogEditTextBinding dialogBinding = + DialogEditTextBinding.inflate(getLayoutInflater()); dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext())); dialogBinding.dialogEditText.setHint(R.string.name); dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT); diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java index f568ef81a..612c38181 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java @@ -9,15 +9,20 @@ import android.view.Window; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.util.StateSaver; import java.util.List; +import java.util.Objects; import java.util.Queue; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.Disposable; @@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave * @param context context used for accessing the database * @param streamEntities used for crating the dialog * @param onExec execution that should occur after a dialog got created, e.g. showing it - * @return Disposable + * @return the disposable that was created */ public static Disposable createCorrespondingDialog( final Context context, final List streamEntities, - final Consumer onExec - ) { + final Consumer onExec) { + return new LocalPlaylistManager(NewPipeDatabase.getInstance(context)) .hasPlaylists() .observeOn(AndroidSchedulers.mainThread()) @@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave : PlaylistCreationDialog.newInstance(streamEntities)) ); } + + /** + * Creates a {@link PlaylistAppendDialog} when playlists exists, + * otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no + * dialog will be created. + * + * @param player the player from which to extract the context and the play queue + * @param fragmentManager the fragment manager to use to show the dialog + * @return the disposable that was created + */ + public static Disposable showForPlayQueue( + final Player player, + @NonNull final FragmentManager fragmentManager) { + + final List streamEntities = Stream.of(player.getPlayQueue()) + .filter(Objects::nonNull) + .flatMap(playQueue -> playQueue.getStreams().stream()) + .map(StreamEntity::new) + .collect(Collectors.toList()); + if (streamEntities.isEmpty()) { + return Disposable.empty(); + } + + return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities, + dialog -> dialog.show(fragmentManager, "PlaylistDialog")); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index 7a8723ceb..07edb0499 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -41,19 +41,15 @@ class FeedDatabaseManager(context: Context) { fun database() = database fun getStreams( - groupId: Long = FeedGroupEntity.GROUP_ALL_ID, - getPlayedStreams: Boolean = true + groupId: Long, + includePlayedStreams: Boolean, + includeFutureStreams: Boolean ): Maybe> { - return when (groupId) { - FeedGroupEntity.GROUP_ALL_ID -> { - if (getPlayedStreams) feedTable.getAllStreams() - else feedTable.getLiveOrNotPlayedStreams() - } - else -> { - if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId) - else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId) - } - } + return feedTable.getStreams( + groupId, + includePlayedStreams, + if (includeFutureStreams) null else OffsetDateTime.now() + ) } fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index b291aa035..c9f926f06 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -41,6 +41,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.edit import androidx.core.os.bundleOf +import androidx.core.view.MenuItemCompat import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager @@ -98,6 +99,7 @@ class FeedFragment : BaseStateFragment() { private lateinit var groupAdapter: GroupieAdapter @State @JvmField var showPlayedItems: Boolean = true + @State @JvmField var showFutureItems: Boolean = true private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null private var updateListViewModeOnResume = false @@ -134,9 +136,10 @@ class FeedFragment : BaseStateFragment() { _feedBinding = FragmentFeedBinding.bind(rootView) super.onViewCreated(rootView, savedInstanceState) - val factory = FeedViewModel.Factory(requireContext(), groupId) - viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java) + val factory = FeedViewModel.getFactory(requireContext(), groupId) + viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java] showPlayedItems = viewModel.getShowPlayedItemsFromPreferences() + showFutureItems = viewModel.getShowFutureItemsFromPreferences() viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } groupAdapter = GroupieAdapter().apply { @@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment() { inflater.inflate(R.menu.menu_feed_fragment, menu) updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items)) + updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items)) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -241,6 +245,11 @@ class FeedFragment : BaseStateFragment() { updateTogglePlayedItemsButton(item) viewModel.togglePlayedItems(showPlayedItems) viewModel.saveShowPlayedItemsToPreferences(showPlayedItems) + } else if (item.itemId == R.id.menu_item_feed_toggle_future_items) { + showFutureItems = !item.isChecked + updateToggleFutureItemsButton(item) + viewModel.toggleFutureItems(showFutureItems) + viewModel.saveShowFutureItemsToPreferences(showFutureItems) } return super.onOptionsItemSelected(item) @@ -278,6 +287,32 @@ class FeedFragment : BaseStateFragment() { requireContext(), if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off ) + MenuItemCompat.setTooltipText( + menuItem, + getString( + if (showPlayedItems) + R.string.feed_toggle_hide_played_items + else + R.string.feed_toggle_show_played_items + ) + ) + } + + private fun updateToggleFutureItemsButton(menuItem: MenuItem) { + menuItem.isChecked = showFutureItems + menuItem.icon = AppCompatResources.getDrawable( + requireContext(), + if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history + ) + MenuItemCompat.setTooltipText( + menuItem, + getString( + if (showFutureItems) + R.string.feed_toggle_hide_future_items + else + R.string.feed_toggle_show_future_items + ) + ) } // ////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index e21963c16..76d5e9d63 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -1,17 +1,20 @@ package org.schabi.newpipe.local.feed +import android.app.Application import android.content.Context import androidx.core.content.edit import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import androidx.preference.PreferenceManager import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.functions.Function4 +import io.reactivex.rxjava3.functions.Function5 import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.App import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.stream.StreamWithState @@ -26,17 +29,23 @@ import java.time.OffsetDateTime import java.util.concurrent.TimeUnit class FeedViewModel( - private val applicationContext: Context, + private val application: Application, groupId: Long = FeedGroupEntity.GROUP_ALL_ID, - initialShowPlayedItems: Boolean = true + initialShowPlayedItems: Boolean = true, + initialShowFutureItems: Boolean = true ) : ViewModel() { - private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) + private val feedDatabaseManager = FeedDatabaseManager(application) private val toggleShowPlayedItems = BehaviorProcessor.create() private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems .startWithItem(initialShowPlayedItems) .distinctUntilChanged() + private val toggleShowFutureItems = BehaviorProcessor.create() + private val toggleShowFutureItemsFlowable = toggleShowFutureItems + .startWithItem(initialShowFutureItems) + .distinctUntilChanged() + private val mutableStateLiveData = MutableLiveData() val stateLiveData: LiveData = mutableStateLiveData @@ -44,21 +53,22 @@ class FeedViewModel( .combineLatest( FeedEventManager.events(), toggleShowPlayedItemsFlowable, + toggleShowFutureItemsFlowable, feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId), - Function4 { t1: FeedEventManager.Event, t2: Boolean, - t3: Long, t4: List -> - return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull()) + Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, + t4: Long, t5: List -> + return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull()) } ) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) - .map { (event, showPlayedItems, notLoadedCount, oldestUpdate) -> + .map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) -> val streamItems = if (event is SuccessResultEvent || event is IdleEvent) feedDatabaseManager - .getStreams(groupId, showPlayedItems) + .getStreams(groupId, showPlayedItems, showFutureItems) .blockingGet(arrayListOf()) else arrayListOf() @@ -89,8 +99,9 @@ class FeedViewModel( private data class CombineResultEventHolder( val t1: FeedEventManager.Event, val t2: Boolean, - val t3: Long, - val t4: OffsetDateTime? + val t3: Boolean, + val t4: Long, + val t5: OffsetDateTime? ) private data class CombineResultDataHolder( @@ -105,31 +116,42 @@ class FeedViewModel( } fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) = - PreferenceManager.getDefaultSharedPreferences(applicationContext).edit { - this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems) + PreferenceManager.getDefaultSharedPreferences(application).edit { + this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems) this.apply() } - fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext) + fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application) + + fun toggleFutureItems(showFutureItems: Boolean) { + toggleShowFutureItems.onNext(showFutureItems) + } + + fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) = + PreferenceManager.getDefaultSharedPreferences(application).edit { + this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems) + this.apply() + } + + fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application) companion object { private fun getShowPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.feed_show_played_items_key), true) - } - - class Factory( - private val context: Context, - private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return FeedViewModel( - context.applicationContext, - groupId, - // Read initial value from preferences - getShowPlayedItemsFromPreferences(context.applicationContext) - ) as T + private fun getShowFutureItemsFromPreferences(context: Context) = + PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.feed_show_future_items_key), true) + fun getFactory(context: Context, groupId: Long) = viewModelFactory { + initializer { + FeedViewModel( + App.getApp(), + groupId, + // Read initial value from preferences + getShowPlayedItemsFromPreferences(context.applicationContext), + getShowFutureItemsFromPreferences(context.applicationContext) + ) + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt index 3a08b3e4a..351975486 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt @@ -4,6 +4,8 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.provider.Settings @@ -11,6 +13,8 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager +import com.squareup.picasso.Picasso +import com.squareup.picasso.Target import org.schabi.newpipe.R import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.service.FeedUpdateInfo @@ -27,6 +31,8 @@ class NotificationHelper(val context: Context) { Context.NOTIFICATION_SERVICE ) as NotificationManager + private val iconLoadingTargets = ArrayList() + /** * Show a notification about new streams from a single channel. * Opening the notification will open the corresponding channel page. @@ -77,10 +83,29 @@ class NotificationHelper(val context: Context) { ) ) - PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap -> - bitmap?.let { builder.setLargeIcon(it) } // set only if != null - manager.notify(data.pseudoId, builder.build()) + // a Target is like a listener for image loading events + val target = object : Target { + override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { + builder.setLargeIcon(bitmap) // set only if there is actually one + manager.notify(data.pseudoId, builder.build()) + iconLoadingTargets.remove(this) // allow it to be garbage-collected + } + + override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) { + manager.notify(data.pseudoId, builder.build()) + iconLoadingTargets.remove(this) // allow it to be garbage-collected + } + + override fun onPrepareLoad(placeHolderDrawable: Drawable) { + // Nothing to do + } } + + // add the target to the list to hold a strong reference and prevent it from being garbage + // collected, since Picasso only holds weak references to targets + iconLoadingTargets.add(target) + + PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target) } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 19f7afce5..b8d2eae2d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -28,7 +28,6 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.feed.dao.FeedDAO; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -51,7 +50,6 @@ import org.schabi.newpipe.util.ExtractorHelper; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import io.reactivex.rxjava3.core.Completable; @@ -89,7 +87,6 @@ public class HistoryRecordManager { * Marks a stream item as watched such that it is hidden from the feed if watched videos are * hidden. Adds a history entry and updates the stream progress to 100%. * - * @see FeedDAO#getLiveOrNotPlayedStreams * @see FeedViewModel#togglePlayedItems * @param info the item to mark as watched * @return a Maybe containing the ID of the item if successful @@ -176,10 +173,6 @@ public class HistoryRecordManager { .subscribeOn(Schedulers.io()); } - public Flowable> getStreamHistory() { - return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); - } - public Flowable> getStreamHistorySortedById() { return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io()); } @@ -188,24 +181,6 @@ public class HistoryRecordManager { return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); } - public Single> insertStreamHistory(final Collection entries) { - final List entities = new ArrayList<>(entries.size()); - for (final StreamHistoryEntry entry : entries) { - entities.add(entry.toStreamHistoryEntity()); - } - return Single.fromCallable(() -> streamHistoryTable.insertAll(entities)) - .subscribeOn(Schedulers.io()); - } - - public Single deleteStreamHistory(final Collection entries) { - final List entities = new ArrayList<>(entries.size()); - for (final StreamHistoryEntry entry : entries) { - entities.add(entry.toStreamHistoryEntity()); - } - return Single.fromCallable(() -> streamHistoryTable.delete(entities)) - .subscribeOn(Schedulers.io()); - } - private boolean isStreamHistoryEnabled() { return sharedPreferences.getBoolean(streamHistoryKey, false); } @@ -259,13 +234,6 @@ public class HistoryRecordManager { // Stream State History /////////////////////////////////////////////////////// - public Maybe getStreamHistory(final StreamInfo info) { - return Maybe.fromCallable(() -> { - final long streamId = streamTable.upsert(new StreamEntity(info)); - return streamHistoryTable.getLatestEntry(streamId); - }).subscribeOn(Schedulers.io()); - } - public Maybe loadStreamState(final PlayQueueItem queueItem) { return queueItem.getStream() .map(info -> streamTable.upsert(new StreamEntity(info))) @@ -311,28 +279,6 @@ public class HistoryRecordManager { }).subscribeOn(Schedulers.io()); } - public Single> loadStreamStateBatch(final List infos) { - return Single.fromCallable(() -> { - final List result = new ArrayList<>(infos.size()); - for (final InfoItem info : infos) { - final List entities = streamTable - .getStream(info.getServiceId(), info.getUrl()).blockingFirst(); - if (entities.isEmpty()) { - result.add(null); - continue; - } - final List states = streamStateTable - .getState(entities.get(0).getUid()).blockingFirst(); - if (states.isEmpty()) { - result.add(null); - } else { - result.add(states.get(0)); - } - } - return result; - }).subscribeOn(Schedulers.io()); - } - public Single> loadLocalStreamStateBatch( final List items) { return Single.fromCallable(() -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 01df34292..a20a80ae9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -135,7 +135,7 @@ public class StatisticsPlaylistFragment protected void initListeners() { super.initListeners(); - itemListAdapter.setSelectedListener(new OnClickGesture() { + itemListAdapter.setSelectedListener(new OnClickGesture<>() { @Override public void selected(final LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 6023d4b10..11d54f1ef 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -1,5 +1,6 @@ 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.util.ThemeHelper.shouldUseGridLayout; @@ -41,15 +42,16 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.dialog.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.MainPlayer.PlayerType; +import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; +import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.ArrayList; import java.util.Collections; @@ -57,10 +59,12 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -163,7 +167,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { + itemListAdapter.setSelectedListener(new OnClickGesture<>() { @Override public void selected(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { @@ -345,7 +349,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Single.just(playlist.stream() + .map(PlaylistStreamEntry::getStreamEntity) + .map(StreamEntity::getUrl) + .collect(Collectors.joining("\n")))) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText), + throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable))); + } + public void removeWatchedStreams(final boolean removePartiallyWatched) { if (isRemovingWatched) { return; @@ -382,8 +402,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment playlistIter = playlist.iterator(); // History data - final HistoryRecordManager recordManager - = new HistoryRecordManager(getContext()); + final HistoryRecordManager recordManager = + new HistoryRecordManager(getContext()); final Iterator historyIter = recordManager .getStreamHistorySortedById().blockingFirst().iterator(); @@ -524,8 +544,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { override fun doInitialLoadLogic() = Unit override fun startLoading(forceLoad: Boolean) = Unit - private val listenerFeedGroups = object : OnClickGesture>() { + private val listenerFeedGroups = object : OnClickGesture> { override fun selected(selectedItem: Item<*>?) { when (selectedItem) { is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name) @@ -361,7 +361,7 @@ class SubscriptionFragment : BaseStateFragment() { } } - private val listenerChannelItem = object : OnClickGesture() { + private val listenerChannelItem = object : OnClickGesture { override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment( fm, selectedItem.serviceId, selectedItem.url, selectedItem.name diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index e96328961..379b4c0d7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -8,12 +8,10 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast -import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.getSystemService import androidx.core.os.bundleOf import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.core.widget.ImageViewCompat import androidx.core.widget.doOnTextChanged import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer @@ -124,14 +122,6 @@ class FeedGroupDialog : DialogFragment(), BackPressable { _feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view) _searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - // KitKat doesn't apply container's theme to content - val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor) - searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor) - searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128)) - ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor) - } - viewModel = ViewModelProvider( this, FeedGroupDialogViewModel.Factory( diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt index 54ba1c6dc..dfdb2b47a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt @@ -122,7 +122,7 @@ class FeedGroupDialogViewModel( private val initialShowOnlyUngrouped: Boolean = false ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return FeedGroupDialogViewModel( context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt index a8c05838f..bee2e910a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt @@ -39,7 +39,7 @@ class ChannelItem( itemChannelDescriptionView.text = infoItem.description } - PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView) + PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView) gesturesListener?.run { viewHolder.root.setOnClickListener { selected(infoItem) } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 063103597..d56d16f3c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -19,6 +19,8 @@ package org.schabi.newpipe.local.subscription.services; +import static org.schabi.newpipe.MainActivity.DEBUG; + import android.content.Intent; import android.net.Uri; import android.util.Log; @@ -43,8 +45,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; -import static org.schabi.newpipe.MainActivity.DEBUG; - public class SubscriptionsExportService extends BaseImportExportService { public static final String KEY_FILE_PATH = "key_file_path"; @@ -109,8 +109,8 @@ public class SubscriptionsExportService extends BaseImportExportService { subscriptionManager.subscriptionTable().getAll().take(1) .map(subscriptionEntities -> { - final List result - = new ArrayList<>(subscriptionEntities.size()); + final List result = + new ArrayList<>(subscriptionEntities.size()); for (final SubscriptionEntity entity : subscriptionEntities) { result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(), entity.getName())); diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java deleted file mode 100644 index a9b9f4c87..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * Part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Binder; -import android.os.IBinder; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; - -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import org.schabi.newpipe.App; -import org.schabi.newpipe.databinding.PlayerBinding; -import org.schabi.newpipe.util.DeviceUtils; -import org.schabi.newpipe.util.ThemeHelper; - -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - - -/** - * One service for all players. - * - * @author mauriciocolli - */ -public final class MainPlayer extends Service { - private static final String TAG = "MainPlayer"; - private static final boolean DEBUG = Player.DEBUG; - - private Player player; - private WindowManager windowManager; - - private final IBinder mBinder = new MainPlayer.LocalBinder(); - - public enum PlayerType { - VIDEO, - AUDIO, - POPUP - } - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - - static final String ACTION_CLOSE - = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE"; - static final String ACTION_PLAY_PAUSE - = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE"; - static final String ACTION_REPEAT - = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT"; - static final String ACTION_PLAY_NEXT - = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT"; - static final String ACTION_PLAY_PREVIOUS - = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS"; - static final String ACTION_FAST_REWIND - = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND"; - static final String ACTION_FAST_FORWARD - = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD"; - static final String ACTION_SHUFFLE - = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE"; - public static final String ACTION_RECREATE_NOTIFICATION - = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; - - /*////////////////////////////////////////////////////////////////////////// - // Service's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate() { - if (DEBUG) { - Log.d(TAG, "onCreate() called"); - } - assureCorrectAppLanguage(this); - windowManager = ContextCompat.getSystemService(this, WindowManager.class); - - ThemeHelper.setTheme(this); - createView(); - } - - private void createView() { - final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this)); - - player = new Player(this); - player.setupFromView(binding); - - NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); - } - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (DEBUG) { - Log.d(TAG, "onStartCommand() called with: intent = [" + intent - + "], flags = [" + flags + "], startId = [" + startId + "]"); - } - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - && player.getPlayQueue() == null) { - // Player is not working, no need to process media button's action - return START_NOT_STICKY; - } - - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - || intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) { - NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); - } - - player.handleIntent(intent); - if (player.getMediaSessionManager() != null) { - player.getMediaSessionManager().handleMediaButtonIntent(intent); - } - return START_NOT_STICKY; - } - - public void stopForImmediateReusing() { - if (DEBUG) { - Log.d(TAG, "stopForImmediateReusing() called"); - } - - if (!player.exoPlayerIsNull()) { - player.saveWasPlaying(); - - // Releases wifi & cpu, disables keepScreenOn, etc. - // We can't just pause the player here because it will make transition - // from one stream to a new stream not smooth - player.smoothStopPlayer(); - player.setRecovery(); - - // Android TV will handle back button in case controls will be visible - // (one more additional unneeded click while the player is hidden) - player.hideControls(0, 0); - player.closeItemsList(); - - // Notification shows information about old stream but if a user selects - // a stream from backStack it's not actual anymore - // So we should hide the notification at all. - // When autoplay enabled such notification flashing is annoying so skip this case - } - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - if (!player.videoPlayerSelected()) { - return; - } - onDestroy(); - // Unload from memory completely - Runtime.getRuntime().halt(0); - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "destroy() called"); - } - cleanup(); - } - - private void cleanup() { - if (player != null) { - // Exit from fullscreen when user closes the player via notification - if (player.isFullscreen()) { - player.toggleFullscreen(); - } - removeViewFromParent(); - - player.saveStreamProgressState(); - player.setRecovery(); - player.stopActivityBinding(); - player.removePopupFromView(); - player.destroy(); - - player = null; - } - } - - public void stopService() { - NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); - cleanup(); - stopSelf(); - } - - @Override - protected void attachBaseContext(final Context base) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); - } - - @Override - public IBinder onBind(final Intent intent) { - return mBinder; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - boolean isLandscape() { - // DisplayMetrics from activity context knows about MultiWindow feature - // while DisplayMetrics from app context doesn't - return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null - ? player.getParentActivity() : this); - } - - @Nullable - public View getView() { - if (player == null) { - return null; - } - - return player.getRootView(); - } - - public void removeViewFromParent() { - if (getView() != null && getView().getParent() != null) { - if (player.getParentActivity() != null) { - // This means view was added to fragment - final ViewGroup parent = (ViewGroup) getView().getParent(); - parent.removeView(getView()); - } else { - // This means view was added by windowManager for popup player - windowManager.removeViewImmediate(getView()); - } - } - } - - - public class LocalBinder extends Binder { - - public MainPlayer getService() { - return MainPlayer.this; - } - - public Player getPlayer() { - return MainPlayer.this.player; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index 676d63458..c18a7f487 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -29,6 +29,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -51,7 +52,7 @@ public final class PlayQueueActivity extends AppCompatActivity private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; - protected Player player; + private Player player; private boolean serviceBound; private ServiceConnection serviceConnection; @@ -126,13 +127,13 @@ public final class PlayQueueActivity extends AppCompatActivity NavigationHelper.openSettings(this); return true; case R.id.action_append_playlist: - player.onAddToPlaylistClicked(getSupportFragmentManager()); + PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager()); return true; case R.id.action_playback_speed: openPlaybackParameterDialog(); return true; case R.id.action_mute: - player.onMuteUnmuteButtonClicked(); + player.toggleMute(); return true; case R.id.action_system_audio: startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); @@ -168,7 +169,7 @@ public final class PlayQueueActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// private void bind() { - final Intent bindIntent = new Intent(this, MainPlayer.class); + final Intent bindIntent = new Intent(this, PlayerService.class); final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE); if (!success) { unbindService(serviceConnection); @@ -184,10 +185,7 @@ public final class PlayQueueActivity extends AppCompatActivity player.removeActivityListener(this); } - if (player != null && player.getPlayQueueAdapter() != null) { - player.getPlayQueueAdapter().unsetSelectedListener(); - } - queueControlBinding.playQueue.setAdapter(null); + onQueueUpdate(null); if (itemTouchHelper != null) { itemTouchHelper.attachToRecyclerView(null); } @@ -208,17 +206,15 @@ public final class PlayQueueActivity extends AppCompatActivity public void onServiceConnected(final ComponentName name, final IBinder service) { Log.d(TAG, "Player service is connected"); - if (service instanceof PlayerServiceBinder) { - player = ((PlayerServiceBinder) service).getPlayerInstance(); - } else if (service instanceof MainPlayer.LocalBinder) { - player = ((MainPlayer.LocalBinder) service).getPlayer(); + if (service instanceof PlayerService.LocalBinder) { + player = ((PlayerService.LocalBinder) service).getPlayer(); } - if (player == null || player.getPlayQueue() == null - || player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) { + if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) { unbind(); finish(); } else { + onQueueUpdate(player.getPlayQueue()); buildComponents(); if (player != null) { player.setActivityListener(PlayQueueActivity.this); @@ -241,7 +237,6 @@ public final class PlayQueueActivity extends AppCompatActivity private void buildQueue() { queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this)); - queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter()); queueControlBinding.playQueue.setClickable(true); queueControlBinding.playQueue.setLongClickable(true); queueControlBinding.playQueue.clearOnScrollListeners(); @@ -249,8 +244,6 @@ public final class PlayQueueActivity extends AppCompatActivity itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue); - - player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener()); } private void buildMetadata() { @@ -370,7 +363,7 @@ public final class PlayQueueActivity extends AppCompatActivity } if (view.getId() == queueControlBinding.controlRepeat.getId()) { - player.onRepeatClicked(); + player.cycleNextRepeatMode(); } else if (view.getId() == queueControlBinding.controlBackward.getId()) { player.playPrevious(); } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) { @@ -382,7 +375,7 @@ public final class PlayQueueActivity extends AppCompatActivity } else if (view.getId() == queueControlBinding.controlForward.getId()) { player.playNext(); } else if (view.getId() == queueControlBinding.controlShuffle.getId()) { - player.onShuffleClicked(); + player.toggleShuffleModeEnabled(); } else if (view.getId() == queueControlBinding.metadata.getId()) { scrollToSelected(); } else if (view.getId() == queueControlBinding.liveSync.getId()) { @@ -445,7 +438,14 @@ public final class PlayQueueActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - public void onQueueUpdate(final PlayQueue queue) { + public void onQueueUpdate(@Nullable final PlayQueue queue) { + if (queue == null) { + queueControlBinding.playQueue.setAdapter(null); + } else { + final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue); + adapter.setSelectedListener(getOnSelectedListener()); + queueControlBinding.playQueue.setAdapter(adapter); + } } @Override @@ -454,7 +454,6 @@ public final class PlayQueueActivity extends AppCompatActivity onStateChanged(state); onPlayModeChanged(repeatMode, shuffled); onPlaybackParameterChanged(parameters); - onMaybePlaybackAdapterChanged(); onMaybeMuteChanged(); } @@ -582,17 +581,6 @@ public final class PlayQueueActivity extends AppCompatActivity } } - private void onMaybePlaybackAdapterChanged() { - if (player == null) { - return; - } - final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter(); - if (maybeNewAdapter != null - && queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) { - queueControlBinding.playQueue.setAdapter(maybeNewAdapter); - } - } - private void onMaybeMuteChanged() { if (menu != null && player != null) { final MenuItem item = menu.findItem(R.id.action_mute); diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index b3194afe6..99d36f66e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -24,196 +24,106 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; import static com.google.android.exoplayer2.Player.DiscontinuityReason; import static com.google.android.exoplayer2.Player.Listener; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.RepeatMode; -import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; -import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; -import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION; -import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; -import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; -import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; -import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; -import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams; -import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; -import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction; -import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight; -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; -import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled; import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode; -import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs; -import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent; -import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE; import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.content.res.Resources; -import android.database.ContentObserver; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.media.AudioManager; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.provider.Settings; -import android.util.DisplayMetrics; import android.util.Log; -import android.util.TypedValue; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.Surface; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnticipateInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.SeekBar; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.appcompat.widget.AppCompatImageButton; -import androidx.appcompat.widget.PopupMenu; -import androidx.collection.ArraySet; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.FragmentManager; +import androidx.core.math.MathUtils; import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.TracksInfo; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.CueGroup; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoSize; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.databinding.PlayerBinding; -import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.Info; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; -import org.schabi.newpipe.info_list.StreamSegmentAdapter; -import org.schabi.newpipe.ktx.AnimationType; -import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.MainPlayer.PlayerType; -import org.schabi.newpipe.player.event.DisplayPortion; import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.event.PlayerGestureListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; -import org.schabi.newpipe.player.helper.MediaSessionManager; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener; -import org.schabi.newpipe.player.listeners.view.QualityClickListener; import org.schabi.newpipe.player.mediaitem.MediaItemTag; +import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; +import org.schabi.newpipe.player.notification.NotificationPlayerUi; import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; -import org.schabi.newpipe.player.playback.PlayerMediaSession; -import org.schabi.newpipe.player.playback.SurfaceHolderCallback; import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; -import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; -import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; +import org.schabi.newpipe.player.ui.MainPlayerUi; +import org.schabi.newpipe.player.ui.PlayerUi; +import org.schabi.newpipe.player.ui.PlayerUiList; +import org.schabi.newpipe.player.ui.PopupPlayerUi; +import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.StreamTypeUtil; -import org.schabi.newpipe.util.external_communication.KoreUtils; -import org.schabi.newpipe.util.external_communication.ShareUtils; -import org.schabi.newpipe.views.ExpandableSurfaceView; -import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; -import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import java.util.stream.IntStream; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -222,14 +132,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.SerialDisposable; -public final class Player implements - PlaybackListener, - Listener, - SeekBar.OnSeekBarChangeListener, - View.OnClickListener, - PopupMenu.OnMenuItemClickListener, - PopupMenu.OnDismissListener, - View.OnLongClickListener { +public final class Player implements PlaybackListener, Listener { public static final boolean DEBUG = MainActivity.DEBUG; public static final String TAG = Player.class.getSimpleName(); @@ -265,18 +168,13 @@ public final class Player implements public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 1000; // 1 second - public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis - public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds - public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds - public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis /*////////////////////////////////////////////////////////////////////////// // Other constants //////////////////////////////////////////////////////////////////////////*/ - private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; - - private static final int RENDERER_UNAVAILABLE = -1; + public static final int RENDERER_UNAVAILABLE = -1; + private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG"; /*////////////////////////////////////////////////////////////////////////// // Playback @@ -284,8 +182,6 @@ public final class Player implements // play queue might be null e.g. while player is starting @Nullable private PlayQueue playQueue; - private PlayQueueAdapter playQueueAdapter; - private StreamSegmentAdapter segmentAdapter; @Nullable private MediaSourceManager playQueueManager; @@ -299,8 +195,6 @@ public final class Player implements private ExoPlayer simpleExoPlayer; private AudioReactor audioReactor; - private MediaSessionManager mediaSessionManager; - @Nullable private SurfaceHolderCallback surfaceHolderCallback; @NonNull private final DefaultTrackSelector trackSelector; @NonNull private final LoadController loadController; @@ -309,13 +203,13 @@ public final class Player implements @NonNull private final VideoPlaybackResolver videoResolver; @NonNull private final AudioPlaybackResolver audioResolver; - private final MainPlayer service; //TODO try to remove and replace everything with context + private final PlayerService service; //TODO try to remove and replace everything with context /*////////////////////////////////////////////////////////////////////////// // Player states //////////////////////////////////////////////////////////////////////////*/ - private PlayerType playerType = PlayerType.VIDEO; + private PlayerType playerType = PlayerType.MAIN; private int currentState = STATE_PREFLIGHT; // audio only mode does not mean that player type is background, but that the player was @@ -323,85 +217,27 @@ public final class Player implements private boolean isAudioOnly = false; private boolean isPrepared = false; private boolean wasPlaying = false; - private boolean isFullscreen = false; - private boolean isVerticalVideo = false; - private boolean fragmentIsVisible = false; - - private List availableStreams; - private int selectedStreamIndex; /*////////////////////////////////////////////////////////////////////////// - // Views + // UIs, listeners and disposables //////////////////////////////////////////////////////////////////////////*/ - private PlayerBinding binding; - - private final Handler controlsVisibilityHandler = new Handler(); - - // fullscreen player - private boolean isQueueVisible = false; - private boolean areSegmentsVisible = false; - private ItemTouchHelper itemTouchHelper; - - /*////////////////////////////////////////////////////////////////////////// - // Popup menus ("popup" means that they pop up, not that they belong to the popup player) - //////////////////////////////////////////////////////////////////////////*/ - - private static final int POPUP_MENU_ID_QUALITY = 69; - private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; - private static final int POPUP_MENU_ID_CAPTION = 89; - - private boolean isSomePopupMenuVisible = false; - private PopupMenu qualityPopupMenu; - private PopupMenu playbackSpeedPopupMenu; - private PopupMenu captionPopupMenu; - - /*////////////////////////////////////////////////////////////////////////// - // Popup player - //////////////////////////////////////////////////////////////////////////*/ - - private PlayerPopupCloseOverlayBinding closeOverlayBinding; - - private boolean isPopupClosing = false; - - private float screenWidth; - private float screenHeight; - - /*////////////////////////////////////////////////////////////////////////// - // Popup player window manager - //////////////////////////////////////////////////////////////////////////*/ - - public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - - @Nullable private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup - @Nullable private final WindowManager windowManager; - - /*////////////////////////////////////////////////////////////////////////// - // Gestures - //////////////////////////////////////////////////////////////////////////*/ - - private static final float MAX_GESTURE_LENGTH = 0.75f; - - private int maxGestureLength; // scaled - private GestureDetector gestureDetector; - private PlayerGestureListener playerGestureListener; - - /*////////////////////////////////////////////////////////////////////////// - // Listeners and disposables - //////////////////////////////////////////////////////////////////////////*/ + @SuppressWarnings({"MemberName", "java:S116"}) // keep the unusual member name + private final PlayerUiList UIs; private BroadcastReceiver broadcastReceiver; private IntentFilter intentFilter; - private PlayerServiceEventListener fragmentListener; - private PlayerEventListener activityListener; - private ContentObserver settingsContentObserver; + @Nullable private PlayerServiceEventListener fragmentListener = null; + @Nullable private PlayerEventListener activityListener = null; @NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); @NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); + // This is the only listener we need for thumbnail loading, since there is always at most only + // one thumbnail being loaded at a time. This field is also here to maintain a strong reference, + // which would otherwise be garbage collected since Picasso holds weak references to targets. + @NonNull private final Target currentThumbnailTarget; + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -410,16 +246,13 @@ public final class Player implements @NonNull private final SharedPreferences prefs; @NonNull private final HistoryRecordManager recordManager; - @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder = - new SeekbarPreviewThumbnailHolder(); - /*////////////////////////////////////////////////////////////////////////// // Constructor //////////////////////////////////////////////////////////////////////////*/ //region Constructor - public Player(@NonNull final MainPlayer service) { + public Player(@NonNull final PlayerService service) { this.service = service; context = service; prefs = PreferenceManager.getDefaultSharedPreferences(context); @@ -436,7 +269,16 @@ public final class Player implements videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); audioResolver = new AudioPlaybackResolver(context, dataSource); - windowManager = ContextCompat.getSystemService(context, WindowManager.class); + currentThumbnailTarget = getCurrentThumbnailTarget(); + + // The UIs added here should always be present. They will be initialized when the player + // reaches the initialization step. Make sure the media session ui is before the + // notification ui in the UIs list, since the notification depends on the media session in + // PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved. + UIs = new PlayerUiList( + new MediaSessionPlayerUi(this), + new NotificationPlayerUi(this) + ); } private VideoPlaybackResolver.QualityResolver getQualityResolver() { @@ -461,234 +303,6 @@ public final class Player implements - /*////////////////////////////////////////////////////////////////////////// - // Setup and initialization - //////////////////////////////////////////////////////////////////////////*/ - //region Setup and initialization - - public void setupFromView(@NonNull final PlayerBinding playerBinding) { - initViews(playerBinding); - if (exoPlayerIsNull()) { - initPlayer(true); - } - initListeners(); - - setupPlayerSeekOverlay(); - } - - private void initViews(@NonNull final PlayerBinding playerBinding) { - binding = playerBinding; - setupSubtitleView(); - - binding.resizeTextView - .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode())); - - binding.playbackSeekBar.getThumb() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); - binding.playbackSeekBar.getProgressDrawable() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)); - - final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getContext(), - R.style.DarkPopupMenu); - - qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView); - playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); - captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); - - binding.progressBarLoadingPanel.getIndeterminateDrawable() - .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); - - binding.titleTextView.setSelected(true); - binding.channelTextView.setSelected(true); - - // Prevent hiding of bottom sheet via swipe inside queue - binding.itemsList.setNestedScrollingEnabled(false); - } - - private void initPlayer(final boolean playOnReady) { - if (DEBUG) { - Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); - } - - simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory) - .setTrackSelector(trackSelector) - .setLoadControl(loadController) - .build(); - simpleExoPlayer.addListener(this); - simpleExoPlayer.setPlayWhenReady(playOnReady); - simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); - simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); - simpleExoPlayer.setHandleAudioBecomingNoisy(true); - - audioReactor = new AudioReactor(context, simpleExoPlayer); - mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, - new PlayerMediaSession(this)); - - registerBroadcastReceiver(); - - // Setup video view - setupVideoSurface(); - - // enable media tunneling - if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.disable_media_tunneling_key), false)) { - Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] " - + "media tunneling disabled in debug preferences"); - } else if (DeviceUtils.shouldSupportMediaTunneling()) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setTunnelingEnabled(true)); - } else if (DEBUG) { - Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling"); - } - } - - private void initListeners() { - binding.qualityTextView.setOnClickListener( - new QualityClickListener(this, qualityPopupMenu)); - binding.playbackSpeed.setOnClickListener( - new PlaybackSpeedClickListener(this, playbackSpeedPopupMenu)); - - binding.playbackSeekBar.setOnSeekBarChangeListener(this); - binding.captionTextView.setOnClickListener(this); - binding.resizeTextView.setOnClickListener(this); - binding.playbackLiveSync.setOnClickListener(this); - - playerGestureListener = new PlayerGestureListener(this, service); - gestureDetector = new GestureDetector(context, playerGestureListener); - binding.getRoot().setOnTouchListener(playerGestureListener); - - binding.queueButton.setOnClickListener(v -> onQueueClicked()); - binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked()); - binding.repeatButton.setOnClickListener(v -> onRepeatClicked()); - binding.shuffleButton.setOnClickListener(v -> onShuffleClicked()); - binding.addToPlaylistButton.setOnClickListener(v -> { - if (getParentActivity() != null) { - onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager()); - } - }); - - binding.playPauseButton.setOnClickListener(this); - binding.playPreviousButton.setOnClickListener(this); - binding.playNextButton.setOnClickListener(this); - - binding.moreOptionsButton.setOnClickListener(this); - binding.moreOptionsButton.setOnLongClickListener(this); - binding.share.setOnClickListener(this); - binding.share.setOnLongClickListener(this); - binding.fullScreenButton.setOnClickListener(this); - binding.screenRotationButton.setOnClickListener(this); - binding.playWithKodi.setOnClickListener(this); - binding.openInBrowser.setOnClickListener(this); - binding.playerCloseButton.setOnClickListener(this); - binding.switchMute.setOnClickListener(this); - - settingsContentObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(final boolean selfChange) { - setupScreenRotationButton(); - } - }; - context.getContentResolver().registerContentObserver( - Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, - settingsContentObserver); - binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange); - - ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> { - final Insets cutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()); - if (!cutout.equals(Insets.NONE)) { - view.setPadding(cutout.left, cutout.top, cutout.right, cutout.bottom); - } - return windowInsets; - }); - - // PlaybackControlRoot already consumed window insets but we should pass them to - // player_overlays and fast_seek_overlay too. Without it they will be off-centered. - binding.playbackControlRoot.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - binding.playerOverlays.setPadding( - v.getPaddingLeft(), - v.getPaddingTop(), - v.getPaddingRight(), - v.getPaddingBottom()); - - // If we added padding to the fast seek overlay, too, it would not go under the - // system ui. Instead we apply negative margins equal to the window insets of - // the opposite side, so that the view covers all of the player (overflowing on - // some sides) and its center coincides with the center of other controls. - final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams) - binding.fastSeekOverlay.getLayoutParams(); - fastSeekParams.leftMargin = -v.getPaddingRight(); - fastSeekParams.topMargin = -v.getPaddingBottom(); - fastSeekParams.rightMargin = -v.getPaddingLeft(); - fastSeekParams.bottomMargin = -v.getPaddingTop(); - }); - } - - /** - * Initializes the Fast-For/Backward overlay. - */ - private void setupPlayerSeekOverlay() { - binding.fastSeekOverlay - .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(this) / 1000) - .performListener(new PlayerFastSeekOverlay.PerformListener() { - - @Override - public void onDoubleTap() { - animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION); - } - - @Override - public void onDoubleTapEnd() { - animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION); - } - - @NonNull - @Override - public FastSeekDirection getFastSeekDirection( - @NonNull final DisplayPortion portion - ) { - if (exoPlayerIsNull()) { - // Abort seeking - playerGestureListener.endMultiDoubleTap(); - return FastSeekDirection.NONE; - } - if (portion == DisplayPortion.LEFT) { - // Check if it's possible to rewind - // Small puffer to eliminate infinite rewind seeking - if (simpleExoPlayer.getCurrentPosition() < 500L) { - return FastSeekDirection.NONE; - } - return FastSeekDirection.BACKWARD; - } else if (portion == DisplayPortion.RIGHT) { - // Check if it's possible to fast-forward - if (currentState == STATE_COMPLETED - || simpleExoPlayer.getCurrentPosition() - >= simpleExoPlayer.getDuration()) { - return FastSeekDirection.NONE; - } - return FastSeekDirection.FORWARD; - } - /* portion == DisplayPortion.MIDDLE */ - return FastSeekDirection.NONE; - } - - @Override - public void seek(final boolean forward) { - playerGestureListener.keepInDoubleTapMode(); - if (forward) { - fastForward(); - } else { - fastRewind(); - } - } - }); - playerGestureListener.doubleTapControls(binding.fastSeekOverlay); - } - - //endregion - - - /*////////////////////////////////////////////////////////////////////////// // Playback initialization via intent //////////////////////////////////////////////////////////////////////////*/ @@ -707,7 +321,8 @@ public final class Player implements } final PlayerType oldPlayerType = playerType; - playerType = retrievePlayerTypeFromIntent(intent); + playerType = PlayerType.retrieveFromIntent(intent); + initUIsForCurrentPlayerType(); // We need to setup audioOnly before super(), see "sourceOf" isAudioOnly = audioPlayerSelected(); @@ -728,9 +343,6 @@ public final class Player implements return; } - // needed for tablets, check the function for a better explanation - directlyOpenFullscreenIfNeeded(); - final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); final float playbackSpeed = savedParameters.speed; final float playbackPitch = savedParameters.pitch; @@ -828,46 +440,39 @@ public final class Player implements reloadPlayQueueManager(); } - setupElementsVisibility(); - setupElementsSize(); - - if (audioPlayerSelected()) { - service.removeViewFromParent(); - } else if (popupPlayerSelected()) { - binding.getRoot().setVisibility(View.VISIBLE); - initPopup(); - initPopupCloseOverlay(); - binding.playPauseButton.requestFocus(); - } else { - binding.getRoot().setVisibility(View.VISIBLE); - initVideoPlayer(); - closeItemsList(); - // Android TV: without it focus will frame the whole player - binding.playPauseButton.requestFocus(); - - // Note: This is for automatically playing (when "Resume playback" is off), see #6179 - if (getPlayWhenReady()) { - play(); - } else { - pause(); - } - } + UIs.call(PlayerUi::setupAfterIntent); NavigationHelper.sendPlayerStartedEvent(context); } - /** - * Open fullscreen on tablets where the option to have the main player start automatically in - * fullscreen mode is on. Rotating the device to landscape is already done in {@link - * VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's - * enough for phones, but not for tablets since the mini player can be also shown in landscape. - */ - private void directlyOpenFullscreenIfNeeded() { - if (fragmentListener != null - && PlayerHelper.isStartMainPlayerFullscreenEnabled(service) - && DeviceUtils.isTablet(service) - && videoPlayerSelected() - && PlayerHelper.globalScreenOrientationLocked(service)) { - fragmentListener.onScreenRotationButtonClicked(); + private void initUIsForCurrentPlayerType() { + if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) + || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) { + // correct UI already in place + return; + } + + // try to reuse binding if possible + final PlayerBinding binding = UIs.get(VideoPlayerUi.class).map(VideoPlayerUi::getBinding) + .orElseGet(() -> { + if (playerType == PlayerType.AUDIO) { + return null; + } else { + return PlayerBinding.inflate(LayoutInflater.from(context)); + } + }); + + switch (playerType) { + case MAIN: + UIs.destroyAll(PopupPlayerUi.class); + UIs.addAndPrepare(new MainPlayerUi(this, binding)); + break; + case POPUP: + UIs.destroyAll(MainPlayerUi.class); + UIs.addAndPrepare(new PopupPlayerUi(this, binding)); + break; + case AUDIO: + UIs.destroyAll(VideoPlayerUi.class); + break; } } @@ -881,23 +486,53 @@ public final class Player implements destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); - // #6825 - Ensure that the shuffle-button is in the correct state on the UI - setShuffleButton(binding.shuffleButton, simpleExoPlayer.getShuffleModeEnabled()); setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); playQueue = queue; playQueue.init(); reloadPlayQueueManager(); - if (playQueueAdapter != null) { - playQueueAdapter.dispose(); - } - playQueueAdapter = new PlayQueueAdapter(context, playQueue); - segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener()); + UIs.call(PlayerUi::initPlayback); simpleExoPlayer.setVolume(isMuted ? 0 : 1); notifyQueueUpdateToListeners(); } + + private void initPlayer(final boolean playOnReady) { + if (DEBUG) { + Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); + } + + simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory) + .setTrackSelector(trackSelector) + .setLoadControl(loadController) + .setUsePlatformDiagnostics(false) + .build(); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setPlayWhenReady(playOnReady); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); + simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); + simpleExoPlayer.setHandleAudioBecomingNoisy(true); + + audioReactor = new AudioReactor(context, simpleExoPlayer); + + registerBroadcastReceiver(); + + // Setup UIs + UIs.call(PlayerUi::initPlayer); + + // enable media tunneling + if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.disable_media_tunneling_key), false)) { + Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] " + + "media tunneling disabled in debug preferences"); + } else if (DeviceUtils.shouldSupportMediaTunneling()) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setTunnelingEnabled(true)); + } else if (DEBUG) { + Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling"); + } + } //endregion @@ -911,8 +546,7 @@ public final class Player implements if (DEBUG) { Log.d(TAG, "destroyPlayer() called"); } - - cleanupVideoSurface(); + UIs.call(PlayerUi::destroyPlayer); if (!exoPlayerIsNull()) { simpleExoPlayer.removeListener(this); @@ -931,32 +565,25 @@ public final class Player implements if (playQueueManager != null) { playQueueManager.dispose(); } - if (mediaSessionManager != null) { - mediaSessionManager.dispose(); - } - - if (playQueueAdapter != null) { - playQueueAdapter.unsetSelectedListener(); - playQueueAdapter.dispose(); - } } public void destroy() { if (DEBUG) { Log.d(TAG, "destroy() called"); } + + saveStreamProgressState(); + setRecovery(); + stopActivityBinding(); + destroyPlayer(); unregisterBroadcastReceiver(); databaseUpdateDisposable.clear(); progressUpdateDisposable.set(null); - PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading + cancelLoadingCurrentThumbnail(); - if (binding != null) { - binding.endScreen.setImageBitmap(null); - } - - context.getContentResolver().unregisterContentObserver(settingsContentObserver); + UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object } public void setRecovery() { @@ -969,11 +596,11 @@ public final class Player implements final long duration = simpleExoPlayer.getDuration(); // No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380 - setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration))); + setRecovery(queuePos, MathUtils.clamp(windowPos, 0, duration)); } private void setRecovery(final int queuePos, final long windowPos) { - if (playQueue.size() <= queuePos) { + if (playQueue == null || playQueue.size() <= queuePos) { return; } @@ -983,7 +610,7 @@ public final class Player implements playQueue.setRecovery(queuePos, windowPos); } - private void reloadPlayQueueManager() { + public void reloadPlayQueueManager() { if (playQueueManager != null) { playQueueManager.dispose(); } @@ -1002,185 +629,11 @@ public final class Player implements service.stopService(); } - public void smoothStopPlayer() { + public void smoothStopForImmediateReusing() { // Pausing would make transition from one stream to a new stream not smooth, so only stop simpleExoPlayer.stop(); - } - //endregion - - - - /*////////////////////////////////////////////////////////////////////////// - // Player type specific setup - //////////////////////////////////////////////////////////////////////////*/ - //region Player type specific setup - - private void initVideoPlayer() { - // restore last resize mode - setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(this)); - binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); - } - - @SuppressLint("RtlHardcoded") - private void initPopup() { - if (DEBUG) { - Log.d(TAG, "initPopup() called"); - } - - // Popup is already added to windowManager - if (popupHasParent()) { - return; - } - - updateScreenSize(); - - popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this); - binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); - - checkPopupPositionBounds(); - - binding.loadingPanel.setMinimumWidth(popupLayoutParams.width); - binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); - - service.removeViewFromParent(); - Objects.requireNonNull(windowManager).addView(binding.getRoot(), popupLayoutParams); - - // Popup doesn't have aspectRatio selector, using FIT automatically - setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); - } - - @SuppressLint("RtlHardcoded") - private void initPopupCloseOverlay() { - if (DEBUG) { - Log.d(TAG, "initPopupCloseOverlay() called"); - } - - // closeOverlayView is already added to windowManager - if (closeOverlayBinding != null) { - return; - } - - closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context)); - - final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams(); - closeOverlayBinding.closeButton.setVisibility(View.GONE); - Objects.requireNonNull(windowManager).addView( - closeOverlayBinding.getRoot(), closeOverlayLayoutParams); - } - //endregion - - - - /*////////////////////////////////////////////////////////////////////////// - // Elements visibility and size: popup and main players have different look - //////////////////////////////////////////////////////////////////////////*/ - //region Elements visibility and size: popup and main players have different look - - /** - * This method ensures that popup and main players have different look. - * We use one layout for both players and need to decide what to show and what to hide. - * Additional measuring should be done inside {@link #setupElementsSize}. - */ - private void setupElementsVisibility() { - if (popupPlayerSelected()) { - binding.fullScreenButton.setVisibility(View.VISIBLE); - binding.screenRotationButton.setVisibility(View.GONE); - binding.resizeTextView.setVisibility(View.GONE); - binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE); - binding.queueButton.setVisibility(View.GONE); - binding.segmentsButton.setVisibility(View.GONE); - binding.moreOptionsButton.setVisibility(View.GONE); - binding.topControls.setOrientation(LinearLayout.HORIZONTAL); - binding.primaryControls.getLayoutParams().width - = LinearLayout.LayoutParams.WRAP_CONTENT; - binding.secondaryControls.setAlpha(1.0f); - binding.secondaryControls.setVisibility(View.VISIBLE); - binding.secondaryControls.setTranslationY(0); - binding.share.setVisibility(View.GONE); - binding.playWithKodi.setVisibility(View.GONE); - binding.openInBrowser.setVisibility(View.GONE); - binding.switchMute.setVisibility(View.GONE); - binding.playerCloseButton.setVisibility(View.GONE); - binding.topControls.bringToFront(); - binding.topControls.setClickable(false); - binding.topControls.setFocusable(false); - binding.bottomControls.bringToFront(); - closeItemsList(); - } else if (videoPlayerSelected()) { - binding.fullScreenButton.setVisibility(View.GONE); - setupScreenRotationButton(); - binding.resizeTextView.setVisibility(View.VISIBLE); - binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); - binding.moreOptionsButton.setVisibility(View.VISIBLE); - binding.topControls.setOrientation(LinearLayout.VERTICAL); - binding.primaryControls.getLayoutParams().width - = LinearLayout.LayoutParams.MATCH_PARENT; - binding.secondaryControls.setVisibility(View.INVISIBLE); - binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context, - R.drawable.ic_expand_more)); - binding.share.setVisibility(View.VISIBLE); - binding.openInBrowser.setVisibility(View.VISIBLE); - binding.switchMute.setVisibility(View.VISIBLE); - binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); - // Top controls have a large minHeight which is allows to drag the player - // down in fullscreen mode (just larger area to make easy to locate by finger) - binding.topControls.setClickable(true); - binding.topControls.setFocusable(true); - } - showHideKodiButton(); - - if (isFullscreen) { - binding.titleTextView.setVisibility(View.VISIBLE); - binding.channelTextView.setVisibility(View.VISIBLE); - } else { - binding.titleTextView.setVisibility(View.GONE); - binding.channelTextView.setVisibility(View.GONE); - } - setMuteButton(binding.switchMute, isMuted()); - - animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); - } - - /** - * Changes padding, size of elements based on player selected right now. - * Popup player has small padding in comparison with the main player - */ - private void setupElementsSize() { - final Resources res = context.getResources(); - final int buttonsMinWidth; - final int playerTopPad; - final int controlsPad; - final int buttonsPad; - - if (popupPlayerSelected()) { - buttonsMinWidth = 0; - playerTopPad = 0; - controlsPad = res.getDimensionPixelSize(R.dimen.player_popup_controls_padding); - buttonsPad = res.getDimensionPixelSize(R.dimen.player_popup_buttons_padding); - } else if (videoPlayerSelected()) { - buttonsMinWidth = res.getDimensionPixelSize(R.dimen.player_main_buttons_min_width); - playerTopPad = res.getDimensionPixelSize(R.dimen.player_main_top_padding); - controlsPad = res.getDimensionPixelSize(R.dimen.player_main_controls_padding); - buttonsPad = res.getDimensionPixelSize(R.dimen.player_main_buttons_padding); - } else { - return; - } - - binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); - binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); - binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); - binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); - binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); - binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); - } - - private void showHideKodiButton() { - // show kodi button if it supports the current service and it is enabled in settings - binding.playWithKodi.setVisibility(videoPlayerSelected() - && playQueue != null && playQueue.getItem() != null - && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) - ? View.VISIBLE : View.GONE); + setRecovery(); + UIs.call(PlayerUi::smoothStopForImmediateReusing); } //endregion @@ -1191,6 +644,12 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Broadcast receiver + /** + * This function prepares the broadcast receiver and is called only in the constructor. + * Therefore if you want any PlayerUi to receive a broadcast action, you should add it here, + * even if that player ui might never be added to the player. In that case the received + * broadcast would not do anything. + */ private void setupBroadcastReceiver() { if (DEBUG) { Log.d(TAG, "setupBroadcastReceiver() called"); @@ -1243,11 +702,6 @@ public final class Player implements break; case ACTION_PLAY_PAUSE: playPause(); - if (!fragmentIsVisible) { - // Ensure that we have audio-only stream playing when a user - // started to play from notification's play button from outside of the app - onFragmentStopped(); - } break; case ACTION_PLAY_PREVIOUS: playPrevious(); @@ -1262,62 +716,20 @@ public final class Player implements fastForward(); break; case ACTION_REPEAT: - onRepeatClicked(); + cycleNextRepeatMode(); break; case ACTION_SHUFFLE: - onShuffleClicked(); - break; - case ACTION_RECREATE_NOTIFICATION: - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); - break; - case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: - fragmentIsVisible = true; - useVideoSource(true); - break; - case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: - fragmentIsVisible = false; - onFragmentStopped(); + toggleShuffleModeEnabled(); break; case Intent.ACTION_CONFIGURATION_CHANGED: assureCorrectAppLanguage(service); if (DEBUG) { - Log.d(TAG, "onConfigurationChanged() called"); + Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received"); } - if (popupPlayerSelected()) { - updateScreenSize(); - changePopupSize(popupLayoutParams.width); - checkPopupPositionBounds(); - } - // Close it because when changing orientation from portrait - // (in fullscreen mode) the size of queue layout can be larger than the screen size - closeItemsList(); - // When the orientation changed, the screen height might be smaller. - // If the end screen thumbnail is not re-scaled, - // it can be larger than the current screen height - // and thus enlarging the whole player. - // This causes the seekbar to be ouf the visible area. - updateEndScreenThumbnail(); - break; - case Intent.ACTION_SCREEN_ON: - // Interrupt playback only when screen turns on - // and user is watching video in popup player. - // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED - if (popupPlayerSelected() && (isPlaying() || isLoading())) { - useVideoSource(true); - } - break; - case Intent.ACTION_SCREEN_OFF: - // Interrupt playback only when screen turns off with popup player working - if (popupPlayerSelected() && (isPlaying() || isLoading())) { - useVideoSource(false); - } - break; - case Intent.ACTION_HEADSET_PLUG: //FIXME - /*notificationManager.cancel(NOTIFICATION_ID); - mediaSessionManager.dispose(); - mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ break; } + + UIs.call(playerUi -> playerUi.onBroadcastReceived(intent)); } private void registerBroadcastReceiver() { @@ -1343,295 +755,72 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Thumbnail loading - private void initThumbnail(final String url) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - initThumbnail() called with url = [" - + (url == null ? "null" : url) + "]"); - } - if (isNullOrEmpty(url)) { - return; - } - - // scale down the notification thumbnail for performance - PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() { + private Target getCurrentThumbnailTarget() { + // a Picasso target is just a listener for thumbnail loading events + return new Target() { @Override public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) { if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url - + "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x" - + bitmap.getHeight() + "], from = [" + from + "]"); + Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap + + " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = [" + + from + "]"); } - - currentThumbnail = bitmap; - NotificationUtil.getInstance() - .createNotificationIfNeededAndUpdate(Player.this, false); - // there is a new thumbnail, so changed the end screen thumbnail, too. - updateEndScreenThumbnail(); + // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. + onThumbnailLoaded(bitmap); } @Override public void onBitmapFailed(final Exception e, final Drawable errorDrawable) { - Log.e(TAG, "Thumbnail - onBitmapFailed() called with: url = [" + url + "]", e); - currentThumbnail = null; - NotificationUtil.getInstance() - .createNotificationIfNeededAndUpdate(Player.this, false); + Log.e(TAG, "Thumbnail - onBitmapFailed() called", e); + // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. + onThumbnailLoaded(null); } @Override public void onPrepareLoad(final Drawable placeHolderDrawable) { if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingStarted() called with: url = [" + url + "]"); + Log.d(TAG, "Thumbnail - onPrepareLoad() called"); } } - }); + }; } - /** - * Scale the player audio / end screen thumbnail down if necessary. - *

- * This is necessary when the thumbnail's height is larger than the device's height - * and thus is enlarging the player's height - * causing the bottom playback controls to be out of the visible screen. - *

- */ - public void updateEndScreenThumbnail() { - if (currentThumbnail == null) { + private void loadCurrentThumbnail(final String url) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = [" + + (url == null ? "null" : url) + "]"); + } + + // first cancel any previous loading + cancelLoadingCurrentThumbnail(); + + // Unset currentThumbnail, since it is now outdated. This ensures it is not used in media + // session metadata while the new thumbnail is being loaded by Picasso. + onThumbnailLoaded(null); + if (isNullOrEmpty(url)) { return; } - final float endScreenHeight = calculateMaxEndScreenThumbnailHeight(); - - final Bitmap endScreenBitmap = Bitmap.createScaledBitmap( - currentThumbnail, - (int) (currentThumbnail.getWidth() - / (currentThumbnail.getHeight() / endScreenHeight)), - (int) endScreenHeight, - true); - - if (DEBUG) { - Log.d(TAG, "Thumbnail - updateEndScreenThumbnail() called with: " - + "currentThumbnail = [" + currentThumbnail + "], " - + currentThumbnail.getWidth() + "x" + currentThumbnail.getHeight() - + ", scaled end screen height = " + endScreenHeight - + ", scaled end screen width = " + endScreenBitmap.getWidth()); - } - - binding.endScreen.setImageBitmap(endScreenBitmap); + // scale down the notification thumbnail for performance + PicassoHelper.loadScaledDownThumbnail(context, url) + .tag(PICASSO_PLAYER_THUMBNAIL_TAG) + .into(currentThumbnailTarget); } - /** - * Calculate the maximum allowed height for the {@link R.id.endScreen} - * to prevent it from enlarging the player. - *

- * The calculating follows these rules: - *

    - *
  • - * Show at least stream title and content creator on TVs and tablets - * when in landscape (always the case for TVs) and not in fullscreen mode. - * This requires to have at least 85dp free space for {@link R.id.detail_root} - * and additional space for the stream title text size - * ({@link R.id.detail_title_root_layout}). - * The text size is 15sp on tablets and 16sp on TVs, - * see {@link R.id.titleTextView}. - *
  • - *
  • - * Otherwise, the max thumbnail height is the screen height. - *
  • - *
- * - * @return the maximum height for the end screen thumbnail - */ - private float calculateMaxEndScreenThumbnailHeight() { - // ensure that screenHeight is initialized and thus not 0 - updateScreenSize(); - - if (DeviceUtils.isTv(context) && !isFullscreen) { - final int videoInfoHeight = - DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context); - return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight); - } else if (DeviceUtils.isTablet(context) && service.isLandscape() && !isFullscreen) { - final int videoInfoHeight = - DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context); - return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight); - } else { // fullscreen player: max height is the device height - return Math.min(currentThumbnail.getHeight(), screenHeight); - } - } - //endregion - - - - /*////////////////////////////////////////////////////////////////////////// - // Popup player utils - //////////////////////////////////////////////////////////////////////////*/ - //region Popup player utils - - /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary - * that goes from (0, 0) to (screenWidth, screenHeight). - *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed - * and {@code true} is returned to represent this change. - *

- */ - public void checkPopupPositionBounds() { - if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: " - + "screenWidth = [" + screenWidth + "], " - + "screenHeight = [" + screenHeight + "]"); - } - if (popupLayoutParams == null) { - return; - } - - if (popupLayoutParams.x < 0) { - popupLayoutParams.x = 0; - } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) { - popupLayoutParams.x = (int) (screenWidth - popupLayoutParams.width); - } - - if (popupLayoutParams.y < 0) { - popupLayoutParams.y = 0; - } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) { - popupLayoutParams.y = (int) (screenHeight - popupLayoutParams.height); - } + private void cancelLoadingCurrentThumbnail() { + // cancel the Picasso job associated with the player thumbnail, if any + PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG); } - public void updateScreenSize() { - if (windowManager != null) { - final DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) { - Log.d(TAG, "updateScreenSize() called: screenWidth = [" - + screenWidth + "], screenHeight = [" + screenHeight + "]"); - } + private void onThumbnailLoaded(@Nullable final Bitmap bitmap) { + // Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the + // thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since + // onThumbnailLoaded won't be called twice with the same nonnull bitmap by Picasso's target. + if (currentThumbnail != bitmap) { + currentThumbnail = bitmap; + UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap)); } } - - /** - * Changes the size of the popup based on the width. - * @param width the new width, height is calculated with - * {@link PlayerHelper#getMinimumVideoHeight(float)} - */ - public void changePopupSize(final int width) { - if (DEBUG) { - Log.d(TAG, "changePopupSize() called with: width = [" + width + "]"); - } - - if (anyPopupViewIsNull()) { - return; - } - - final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width); - final int actualWidth = (int) (width > screenWidth ? screenWidth - : (width < minimumWidth ? minimumWidth : width)); - final int actualHeight = (int) getMinimumVideoHeight(width); - if (DEBUG) { - Log.d(TAG, "updatePopupSize() updated values:" - + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); - } - - popupLayoutParams.width = actualWidth; - popupLayoutParams.height = actualHeight; - binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); - Objects.requireNonNull(windowManager) - .updateViewLayout(binding.getRoot(), popupLayoutParams); - } - - private void changePopupWindowFlags(final int flags) { - if (DEBUG) { - Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]"); - } - - if (!anyPopupViewIsNull()) { - popupLayoutParams.flags = flags; - Objects.requireNonNull(windowManager) - .updateViewLayout(binding.getRoot(), popupLayoutParams); - } - } - - public void closePopup() { - if (DEBUG) { - Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); - } - if (isPopupClosing) { - return; - } - isPopupClosing = true; - - saveStreamProgressState(); - Objects.requireNonNull(windowManager).removeView(binding.getRoot()); - - animatePopupOverlayAndFinishService(); - } - - public void removePopupFromView() { - if (windowManager != null) { - // wrap in try-catch since it could sometimes generate errors randomly - try { - if (popupHasParent()) { - windowManager.removeView(binding.getRoot()); - } - } catch (final IllegalArgumentException e) { - Log.w(TAG, "Failed to remove popup from window manager", e); - } - - try { - final boolean closeOverlayHasParent = closeOverlayBinding != null - && closeOverlayBinding.getRoot().getParent() != null; - if (closeOverlayHasParent) { - windowManager.removeView(closeOverlayBinding.getRoot()); - } - } catch (final IllegalArgumentException e) { - Log.w(TAG, "Failed to remove popup overlay from window manager", e); - } - } - } - - private void animatePopupOverlayAndFinishService() { - final int targetTranslationY = - (int) (closeOverlayBinding.closeButton.getRootView().getHeight() - - closeOverlayBinding.closeButton.getY()); - - closeOverlayBinding.closeButton.animate().setListener(null).cancel(); - closeOverlayBinding.closeButton.animate() - .setInterpolator(new AnticipateInterpolator()) - .translationY(targetTranslationY) - .setDuration(400) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(final Animator animation) { - end(); - } - - @Override - public void onAnimationEnd(final Animator animation) { - end(); - } - - private void end() { - Objects.requireNonNull(windowManager) - .removeView(closeOverlayBinding.getRoot()); - closeOverlayBinding = null; - service.stopService(); - } - }).start(); - } - - private boolean popupHasParent() { - return binding != null - && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams - && binding.getRoot().getParent() != null; - } - - private boolean anyPopupViewIsNull() { - // TODO understand why checking getParentActivity() != null - return popupLayoutParams == null || windowManager == null - || getParentActivity() != null || binding.getRoot().getParent() == null; - } //endregion @@ -1645,7 +834,7 @@ public final class Player implements return getPlaybackParameters().speed; } - private void setPlaybackSpeed(final float speed) { + public void setPlaybackSpeed(final float speed) { setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); } @@ -1694,40 +883,13 @@ public final class Player implements private void onUpdateProgress(final int currentProgress, final int duration, final int bufferPercent) { - if (!isPrepared) { - return; - } - - if (duration != binding.playbackSeekBar.getMax()) { - setVideoDurationToControls(duration); - } - if (currentState != STATE_PAUSED) { - updatePlayBackElementsCurrentDuration(currentProgress); - } - if (simpleExoPlayer.isLoading() || bufferPercent > 90) { - binding.playbackSeekBar.setSecondaryProgress( - (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100))); - } - if (DEBUG && bufferPercent % 20 == 0) { //Limit log - Log.d(TAG, "notifyProgressUpdateToListeners() called with: " - + "isVisible = " + isControlsVisible() + ", " - + "currentProgress = [" + currentProgress + "], " - + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); - } - binding.playbackLiveSync.setClickable(!isLiveEdge()); - - notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent); - - if (areSegmentsVisible) { - segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress)); - } - - if (isQueueVisible) { - updateQueueTime(currentProgress); + if (isPrepared) { + UIs.call(ui -> ui.onUpdateProgress(currentProgress, duration, bufferPercent)); + notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent); } } - private void startProgressLoop() { + public void startProgressLoop() { progressUpdateDisposable.set(getProgressUpdateDisposable()); } @@ -1735,11 +897,11 @@ public final class Player implements progressUpdateDisposable.set(null); } - private boolean isProgressLoopRunning() { + public boolean isProgressLoopRunning() { return progressUpdateDisposable.get() != null; } - private void triggerProgressUpdate() { + public void triggerProgressUpdate() { if (exoPlayerIsNull()) { return; } @@ -1756,229 +918,12 @@ public final class Player implements error -> Log.e(TAG, "Progress update failure: ", error)); } - @Override // seekbar listener - public void onProgressChanged(final SeekBar seekBar, final int progress, - final boolean fromUser) { - // Currently we don't need method execution when fromUser is false - if (!fromUser) { - return; - } - if (DEBUG) { - Log.d(TAG, "onProgressChanged() called with: " - + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); - } - - binding.currentDisplaySeek.setText(getTimeString(progress)); - - // Seekbar Preview Thumbnail - SeekbarPreviewThumbnailHelper - .tryResizeAndSetSeekbarPreviewThumbnail( - getContext(), - seekbarPreviewThumbnailHolder.getBitmapAt(progress), - binding.currentSeekbarPreviewThumbnail, - binding.subtitleView::getWidth); - - adjustSeekbarPreviewContainer(); - } - - private void adjustSeekbarPreviewContainer() { - try { - // Should only be required when an error occurred before - // and the layout was positioned in the center - binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY); - - // Calculate the current left position of seekbar progress in px - // More info: https://stackoverflow.com/q/20493577 - final int currentSeekbarLeft = - binding.playbackSeekBar.getLeft() - + binding.playbackSeekBar.getPaddingLeft() - + binding.playbackSeekBar.getThumb().getBounds().left; - - // Calculate the (unchecked) left position of the container - final int uncheckedContainerLeft = - currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2); - - // Fix the position so it's within the boundaries - final int checkedContainerLeft = - Math.max( - Math.min( - uncheckedContainerLeft, - // Max left - binding.playbackWindowRoot.getWidth() - - binding.seekbarPreviewContainer.getWidth() - ), - 0 // Min left - ); - - // See also: https://stackoverflow.com/a/23249734 - final LinearLayout.LayoutParams params = - new LinearLayout.LayoutParams( - binding.seekbarPreviewContainer.getLayoutParams()); - params.setMarginStart(checkedContainerLeft); - binding.seekbarPreviewContainer.setLayoutParams(params); - } catch (final Exception ex) { - Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex); - // Fallback - position in the middle - binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER); - } - } - - @Override // seekbar listener - public void onStartTrackingTouch(final SeekBar seekBar) { - if (DEBUG) { - Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); - } - if (currentState != STATE_PAUSED_SEEK) { - changeState(STATE_PAUSED_SEEK); - } - - saveWasPlaying(); - if (isPlaying()) { - simpleExoPlayer.pause(); - } - - showControls(0); - animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION, - AnimationType.SCALE_AND_ALPHA); - animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION, - AnimationType.SCALE_AND_ALPHA); - } - - @Override // seekbar listener - public void onStopTrackingTouch(final SeekBar seekBar) { - if (DEBUG) { - Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); - } - - seekTo(seekBar.getProgress()); - if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) { - simpleExoPlayer.play(); - } - - binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); - animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); - animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA); - - if (currentState == STATE_PAUSED_SEEK) { - changeState(STATE_BUFFERING); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - if (wasPlaying) { - showControlsThenHide(); - } - } - public void saveWasPlaying() { this.wasPlaying = getPlayWhenReady(); } - //endregion - - - /*////////////////////////////////////////////////////////////////////////// - // Controls showing / hiding - //////////////////////////////////////////////////////////////////////////*/ - //region Controls showing / hiding - - public boolean isControlsVisible() { - return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE; - } - - public void showControlsThenHide() { - if (DEBUG) { - Log.d(TAG, "showControlsThenHide() called"); - } - showOrHideButtons(); - showSystemUIPartially(); - - final int hideTime = binding.playbackControlRoot.isInTouchMode() - ? DEFAULT_CONTROLS_HIDE_TIME - : DPAD_CONTROLS_HIDE_TIME; - - showHideShadow(true, DEFAULT_CONTROLS_DURATION); - animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, - AnimationType.ALPHA, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); - } - - public void showControls(final long duration) { - if (DEBUG) { - Log.d(TAG, "showControls() called"); - } - showOrHideButtons(); - showSystemUIPartially(); - controlsVisibilityHandler.removeCallbacksAndMessages(null); - showHideShadow(true, duration); - animate(binding.playbackControlRoot, true, duration); - } - - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: duration = [" + duration - + "], delay = [" + delay + "]"); - } - - showOrHideButtons(); - - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(() -> { - showHideShadow(false, duration); - animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA, - 0, this::hideSystemUIIfNeeded); - }, delay); - } - - public void showHideShadow(final boolean show, final long duration) { - animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null); - animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null); - animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null); - } - - private void showOrHideButtons() { - if (playQueue == null) { - return; - } - - final boolean showPrev = playQueue.getIndex() != 0; - final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); - final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); - /* only when stream has segments and is not playing in popup player */ - final boolean showSegment = !popupPlayerSelected() - && !getCurrentStreamInfo() - .map(StreamInfo::getStreamSegments) - .map(List::isEmpty) - .orElse(/*no stream info=*/true); - - binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); - binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); - binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); - binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f); - binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); - binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f); - binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE); - binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f); - } - - private void showSystemUIPartially() { - final AppCompatActivity activity = getParentActivity(); - if (isFullscreen && activity != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.getWindow().setStatusBarColor(Color.TRANSPARENT); - activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); - } - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - activity.getWindow().getDecorView().setSystemUiVisibility(visibility); - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - } - - private void hideSystemUIIfNeeded() { - if (fragmentListener != null) { - fragmentListener.hideSystemUiIfNeeded(); - } + public boolean wasPlaying() { + return wasPlaying; } //endregion @@ -2012,7 +957,7 @@ public final class Player implements private void updatePlaybackState(final boolean playWhenReady, final int playbackState) { if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + Log.d(TAG, "ExoPlayer - updatePlaybackState() called with: " + "playWhenReady = [" + playWhenReady + "], " + "playbackState = [" + playbackState + "]"); } @@ -2123,9 +1068,7 @@ public final class Player implements Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); } - setVideoDurationToControls((int) simpleExoPlayer.getDuration()); - - binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); + UIs.call(PlayerUi::onPrepared); if (playWhenReady) { audioReactor.requestAudioFocus(); @@ -2140,22 +1083,7 @@ public final class Player implements startProgressLoop(); } - // if we are e.g. switching players, hide controls - hideControls(DEFAULT_CONTROLS_DURATION, 0); - - binding.playbackSeekBar.setEnabled(false); - binding.playbackSeekBar.getThumb() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); - - binding.loadingPanel.setBackgroundColor(Color.BLACK); - animate(binding.loadingPanel, true, 0); - animate(binding.surfaceForeground, true, 100); - - binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow); - animatePlayButtons(false, 100); - binding.getRoot().setKeepScreenOn(false); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + UIs.call(PlayerUi::onBlocked); } private void onPlaying() { @@ -2166,44 +1094,15 @@ public final class Player implements startProgressLoop(); } - updateStreamRelatedViews(); - - binding.playbackSeekBar.setEnabled(true); - binding.playbackSeekBar.getThumb() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); - - binding.loadingPanel.setVisibility(View.GONE); - - animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); - - animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0, - () -> { - binding.playPauseButton.setImageResource(R.drawable.ic_pause); - animatePlayButtons(true, 200); - if (!isQueueVisible) { - binding.playPauseButton.requestFocus(); - } - }); - - changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); - checkLandscape(); - binding.getRoot().setKeepScreenOn(true); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + UIs.call(PlayerUi::onPlaying); } private void onBuffering() { if (DEBUG) { Log.d(TAG, "onBuffering() called"); } - binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT); - binding.loadingPanel.setVisibility(View.VISIBLE); - binding.getRoot().setKeepScreenOn(true); - - if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } + UIs.call(PlayerUi::onBuffering); } private void onPaused() { @@ -2215,43 +1114,14 @@ public final class Player implements stopProgressLoop(); } - // Don't let UI elements popup during double tap seeking. This state is entered sometimes - // during seeking/loading. This if-else check ensures that the controls aren't popping up. - if (!playerGestureListener.isDoubleTapping()) { - showControls(400); - binding.loadingPanel.setVisibility(View.GONE); - - animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0, - () -> { - binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow); - animatePlayButtons(true, 200); - if (!isQueueVisible) { - binding.playPauseButton.requestFocus(); - } - }); - } - changePopupWindowFlags(IDLE_WINDOW_FLAGS); - - // Remove running notification when user does not want minimization to background or popup - if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE - && videoPlayerSelected()) { - NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); - } else { - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - binding.getRoot().setKeepScreenOn(false); + UIs.call(PlayerUi::onPaused); } private void onPausedSeek() { if (DEBUG) { Log.d(TAG, "onPausedSeek() called"); } - - animatePlayButtons(false, 100); - binding.getRoot().setKeepScreenOn(true); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + UIs.call(PlayerUi::onPausedSeek); } private void onCompleted() { @@ -2262,19 +1132,7 @@ public final class Player implements return; } - animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0, - () -> { - binding.playPauseButton.setImageResource(R.drawable.ic_replay); - animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); - }); - - binding.getRoot().setKeepScreenOn(false); - changePopupWindowFlags(IDLE_WINDOW_FLAGS); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - if (isFullscreen) { - toggleFullscreen(); - } + UIs.call(PlayerUi::onCompleted); if (playQueue.getIndex() < playQueue.size() - 1) { playQueue.offsetIndex(+1); @@ -2282,38 +1140,6 @@ public final class Player implements if (isProgressLoopRunning()) { stopProgressLoop(); } - - // When a (short) video ends the elements have to display the correct values - see #6180 - updatePlayBackElementsCurrentDuration(binding.playbackSeekBar.getMax()); - - showControls(500); - animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); - binding.loadingPanel.setVisibility(View.GONE); - animate(binding.surfaceForeground, true, 100); - } - - private void animatePlayButtons(final boolean show, final int duration) { - animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA); - - boolean showQueueButtons = show; - if (playQueue == null) { - showQueueButtons = false; - } - - if (!showQueueButtons || playQueue.getIndex() > 0) { - animate( - binding.playPreviousButton, - showQueueButtons, - duration, - AnimationType.SCALE_AND_ALPHA); - } - if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) { - animate( - binding.playNextButton, - showQueueButtons, - duration, - AnimationType.SCALE_AND_ALPHA); - } } //endregion @@ -2324,43 +1150,29 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Repeat and shuffle - public void onRepeatClicked() { - if (DEBUG) { - Log.d(TAG, "onRepeatClicked() called"); - } - setRepeatMode(nextRepeatMode(getRepeatMode())); - } - - public void onShuffleClicked() { - if (DEBUG) { - Log.d(TAG, "onShuffleClicked() called"); - } - - if (exoPlayerIsNull()) { - return; - } - simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); - } - @RepeatMode public int getRepeatMode() { return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); } - private void setRepeatMode(@RepeatMode final int repeatMode) { + public void setRepeatMode(@RepeatMode final int repeatMode) { if (!exoPlayerIsNull()) { simpleExoPlayer.setRepeatMode(repeatMode); } } + public void cycleNextRepeatMode() { + setRepeatMode(nextRepeatMode(getRepeatMode())); + } + @Override public void onRepeatModeChanged(@RepeatMode final int repeatMode) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + "repeatMode = [" + repeatMode + "]"); } - setRepeatModeButton(binding.repeatButton, repeatMode); - onShuffleOrRepeatModeChanged(); + UIs.call(playerUi -> playerUi.onRepeatModeChanged(repeatMode)); + notifyPlaybackUpdateToListeners(); } @Override @@ -2378,57 +1190,13 @@ public final class Player implements } } - setShuffleButton(binding.shuffleButton, shuffleModeEnabled); - onShuffleOrRepeatModeChanged(); - } - - private void onShuffleOrRepeatModeChanged() { + UIs.call(playerUi -> playerUi.onShuffleModeEnabledChanged(shuffleModeEnabled)); notifyPlaybackUpdateToListeners(); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); } - private void setRepeatModeButton(final AppCompatImageButton imageButton, - @RepeatMode final int repeatMode) { - switch (repeatMode) { - case REPEAT_MODE_OFF: - imageButton.setImageResource(R.drawable.exo_controls_repeat_off); - break; - case REPEAT_MODE_ONE: - imageButton.setImageResource(R.drawable.exo_controls_repeat_one); - break; - case REPEAT_MODE_ALL: - imageButton.setImageResource(R.drawable.exo_controls_repeat_all); - break; - } - } - - private void setShuffleButton(@NonNull final ImageButton button, final boolean shuffled) { - button.setImageAlpha(shuffled ? 255 : 77); - } - //endregion - - - - /*////////////////////////////////////////////////////////////////////////// - // Playlist append - //////////////////////////////////////////////////////////////////////////*/ - //region Playlist append - - public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) { - if (DEBUG) { - Log.d(TAG, "onAddToPlaylistClicked() called"); - } - - if (getPlayQueue() != null) { - PlaylistDialog.createCorrespondingDialog( - getContext(), - getPlayQueue() - .getStreams() - .stream() - .map(StreamEntity::new) - .collect(Collectors.toList()), - dialog -> dialog.show(fragmentManager, TAG) - ); + public void toggleShuffleModeEnabled() { + if (!exoPlayerIsNull()) { + simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); } } //endregion @@ -2440,23 +1208,16 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Mute / Unmute - public void onMuteUnmuteButtonClicked() { - if (DEBUG) { - Log.d(TAG, "onMuteUnmuteButtonClicked() called"); - } - simpleExoPlayer.setVolume(isMuted() ? 1 : 0); + public void toggleMute() { + final boolean wasMuted = isMuted(); + simpleExoPlayer.setVolume(wasMuted ? 1 : 0); + UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted)); notifyPlaybackUpdateToListeners(); - setMuteButton(binding.switchMute, isMuted()); } - boolean isMuted() { + public boolean isMuted() { return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0; } - - private void setMuteButton(@NonNull final ImageButton button, final boolean isMuted) { - button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted - ? R.drawable.ic_volume_off : R.drawable.ic_volume_up)); - } //endregion @@ -2515,12 +1276,12 @@ public final class Player implements } @Override - public void onTracksInfoChanged(@NonNull final TracksInfo tracksInfo) { + public void onTracksChanged(@NonNull final Tracks tracks) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onTracksChanged(), " - + "track group size = " + tracksInfo.getTrackGroupInfos().size()); + + "track group size = " + tracks.getGroups().size()); } - onTextTracksChanged(tracksInfo); + UIs.call(playerUi -> playerUi.onTextTracksChanged(tracks)); } @Override @@ -2529,7 +1290,7 @@ public final class Player implements Log.d(TAG, "ExoPlayer - playbackParameters(), speed = [" + playbackParameters.speed + "], pitch = [" + playbackParameters.pitch + "]"); } - binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed)); + UIs.call(playerUi -> playerUi.onPlaybackParametersChanged(playbackParameters)); } @Override @@ -2581,13 +1342,12 @@ public final class Player implements @Override public void onRenderedFirstFrame() { - //TODO check if this causes black screen when switching to fullscreen - animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION); + UIs.call(PlayerUi::onRenderedFirstFrame); } @Override - public void onCues(@NonNull final List cues) { - binding.subtitleView.onCues(cues); + public void onCues(@NonNull final CueGroup cueGroup) { + UIs.call(playerUi -> playerUi.onCues(cueGroup.cues)); } //endregion @@ -2628,7 +1388,7 @@ public final class Player implements // Any error code not explicitly covered here are either unrelated to NewPipe use case // (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should // shutdown. - @SuppressLint("SwitchIntDef") + @SuppressWarnings("SwitchIntDef") @Override public void onPlayerError(@NonNull final PlaybackException error) { Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error); @@ -2707,18 +1467,6 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Playback position and seek - /** - * Sets the current duration into the corresponding elements. - * @param currentProgress - */ - private void updatePlayBackElementsCurrentDuration(final int currentProgress) { - // Don't set seekbar progress while user is seeking - if (currentState != STATE_PAUSED_SEEK) { - binding.playbackSeekBar.setProgress(currentProgress); - } - binding.playbackCurrentTime.setText(getTimeString(currentProgress)); - } - @Override // own playback listener (this is a getter) public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { // If live, then not near playback edge @@ -2761,48 +1509,50 @@ public final class Player implements Log.d(TAG, "Playback - onPlaybackSynchronize(was blocked: " + wasBlocked + ") called with item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); } - if (exoPlayerIsNull() || playQueue == null) { - return; + if (exoPlayerIsNull() || playQueue == null || currentItem == item) { + return; // nothing to synchronize } - final boolean hasPlayQueueItemChanged = currentItem != item; + final int playQueueIndex = playQueue.indexOf(item); + final int playlistIndex = simpleExoPlayer.getCurrentMediaItemIndex(); + final int playlistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); + final boolean removeThumbnailBeforeSync = currentItem == null + || currentItem.getServiceId() != item.getServiceId() + || !currentItem.getUrl().equals(item.getUrl()); - final int currentPlayQueueIndex = playQueue.indexOf(item); - final int currentPlaylistIndex = simpleExoPlayer.getCurrentMediaItemIndex(); - final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); - - // If nothing to synchronize - if (!hasPlayQueueItemChanged) { - return; - } currentItem = item; - // Check if on wrong window - if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Playback - Play Queue may be desynchronized: item " - + "index=[" + currentPlayQueueIndex + "], " - + "queue index=[" + playQueue.getIndex() + "]"); + if (playQueueIndex != playQueue.getIndex()) { + // wrong window (this should be impossible, as this method is called with + // `item=playQueue.getItem()`, so the index of that item must be equal to `getIndex()`) + Log.e(TAG, "Playback - Play Queue may be not in sync: item index=[" + + playQueueIndex + "], " + "queue index=[" + playQueue.getIndex() + "]"); - // Check if bad seek position - } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) - || currentPlayQueueIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to invalid " - + "index=[" + currentPlayQueueIndex + "] with " - + "playlist length=[" + currentPlaylistSize + "]"); + } else if ((playlistSize > 0 && playQueueIndex >= playlistSize) || playQueueIndex < 0) { + // the queue and the player's timeline are not in sync, since the play queue index + // points outside of the timeline + Log.e(TAG, "Playback - Trying to seek to invalid index=[" + playQueueIndex + + "] with playlist length=[" + playlistSize + "]"); - } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { + } else if (wasBlocked || playlistIndex != playQueueIndex || !isPlaying()) { + // either the player needs to be unblocked, or the play queue index has just been + // changed and needs to be synchronized, or the player is not playing if (DEBUG) { - Log.d(TAG, "Playback - Rewinding to correct " - + "index=[" + currentPlayQueueIndex + "], " - + "from=[" + currentPlaylistIndex + "], " - + "size=[" + currentPlaylistSize + "]."); + Log.d(TAG, "Playback - Rewinding to correct index=[" + playQueueIndex + "], " + + "from=[" + playlistIndex + "], size=[" + playlistSize + "]."); } + if (removeThumbnailBeforeSync) { + // unset the current (now outdated) thumbnail to ensure it is not used during sync + onThumbnailLoaded(null); + } + + // sync the player index with the queue index, and seek to the correct position if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { - simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); - playQueue.unsetRecovery(currentPlayQueueIndex); + simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition()); + playQueue.unsetRecovery(playQueueIndex); } else { - simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); + simpleExoPlayer.seekToDefaultPosition(playQueueIndex); } } } @@ -2813,14 +1563,8 @@ public final class Player implements } if (!exoPlayerIsNull()) { // prevent invalid positions when fast-forwarding/-rewinding - long normalizedPositionMillis = positionMillis; - if (normalizedPositionMillis < 0) { - normalizedPositionMillis = 0; - } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { - normalizedPositionMillis = simpleExoPlayer.getDuration(); - } - - simpleExoPlayer.seekTo(normalizedPositionMillis); + simpleExoPlayer.seekTo(MathUtils.clamp(positionMillis, 0, + simpleExoPlayer.getDuration())); } } @@ -2836,20 +1580,6 @@ public final class Player implements simpleExoPlayer.seekToDefaultPosition(); } } - - /** - * Sets the video duration time into all control components (e.g. seekbar). - * @param duration - */ - private void setVideoDurationToControls(final int duration) { - binding.playbackEndTime.setText(getTimeString(duration)); - - binding.playbackSeekBar.setMax(duration); - // This is important for Android TVs otherwise it would apply the default from - // setMax/Min methods which is (max - min) / 20 - binding.playbackSeekBar.setKeyProgressIncrement( - PlayerHelper.retrieveSeekDurationFromPreferences(this)); - } //endregion @@ -2973,6 +1703,7 @@ public final class Player implements } private void saveStreamProgressState(final long progressMillis) { + //noinspection SimplifyOptionalCallChains if (!getCurrentStreamInfo().isPresent() || !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { return; @@ -3022,66 +1753,33 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Metadata - private void onMetadataChanged(@NonNull final StreamInfo info) { + private void updateMetadataWith(@NonNull final StreamInfo info) { if (DEBUG) { Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); } - - initThumbnail(info.getThumbnailUrl()); - registerStreamViewed(); - updateStreamRelatedViews(); - showHideKodiButton(); - - binding.titleTextView.setText(info.getName()); - binding.channelTextView.setText(info.getUploaderName()); - - this.seekbarPreviewThumbnailHolder.resetFrom(this.getContext(), info.getPreviewFrames()); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - - final boolean showThumbnail = prefs.getBoolean( - context.getString(R.string.show_thumbnail_key), true); - mediaSessionManager.setMetadata( - getVideoTitle(), - getUploaderName(), - showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(), - StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration() - ); - - notifyMetadataUpdateToListeners(); - - if (areSegmentsVisible) { - if (segmentAdapter.setItems(info)) { - final int adapterPosition = getNearestStreamSegmentPosition( - simpleExoPlayer.getCurrentPosition()); - segmentAdapter.selectSegmentAt(adapterPosition); - binding.itemsList.scrollToPosition(adapterPosition); - } else { - closeItemsList(); - } - } - } - - private void updateMetadataWith(@NonNull final StreamInfo streamInfo) { if (exoPlayerIsNull()) { return; } - maybeAutoQueueNextStream(streamInfo); - onMetadataChanged(streamInfo); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + maybeAutoQueueNextStream(info); + + loadCurrentThumbnail(info.getThumbnailUrl()); + registerStreamViewed(); + + notifyMetadataUpdateToListeners(); + UIs.call(playerUi -> playerUi.onMetadataChanged(info)); } @NonNull - private String getVideoUrl() { + public String getVideoUrl() { return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getStreamUrl(); } @NonNull - private String getVideoUrlAtCurrentTime() { - final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000; + public String getVideoUrlAtCurrentTime() { + final long timeSeconds = simpleExoPlayer.getCurrentPosition() / 1000; String videoUrl = getVideoUrl(); if (!isLive() && timeSeconds >= 0 && currentMetadata != null && currentMetadata.getServiceId() == YouTube.getServiceId()) { @@ -3107,10 +1805,6 @@ public final class Player implements @Nullable public Bitmap getThumbnail() { - if (currentThumbnail == null) { - currentThumbnail = BitmapFactory.decodeResource( - context.getResources(), R.drawable.dummy_thumbnail); - } return currentThumbnail; } //endregion @@ -3157,188 +1851,7 @@ public final class Player implements @Override public void onPlayQueueEdited() { notifyPlaybackUpdateToListeners(); - showOrHideButtons(); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - private void onQueueClicked() { - isQueueVisible = true; - - hideSystemUIIfNeeded(); - buildQueue(); - - binding.itemsListHeaderTitle.setVisibility(View.GONE); - binding.itemsListHeaderDuration.setVisibility(View.VISIBLE); - binding.shuffleButton.setVisibility(View.VISIBLE); - binding.repeatButton.setVisibility(View.VISIBLE); - binding.addToPlaylistButton.setVisibility(View.VISIBLE); - - hideControls(0, 0); - binding.itemsListPanel.requestFocus(); - animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION, - AnimationType.SLIDE_AND_ALPHA); - - binding.itemsList.scrollToPosition(playQueue.getIndex()); - - updateQueueTime((int) simpleExoPlayer.getCurrentPosition()); - } - - private void buildQueue() { - binding.itemsList.setAdapter(playQueueAdapter); - binding.itemsList.setClickable(true); - binding.itemsList.setLongClickable(true); - - binding.itemsList.clearOnScrollListeners(); - binding.itemsList.addOnScrollListener(getQueueScrollListener()); - - itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(binding.itemsList); - - playQueueAdapter.setSelectedListener(getOnSelectedListener()); - - binding.itemsListClose.setOnClickListener(view -> closeItemsList()); - } - - private void onSegmentsClicked() { - areSegmentsVisible = true; - - hideSystemUIIfNeeded(); - buildSegments(); - - binding.itemsListHeaderTitle.setVisibility(View.VISIBLE); - binding.itemsListHeaderDuration.setVisibility(View.GONE); - binding.shuffleButton.setVisibility(View.GONE); - binding.repeatButton.setVisibility(View.GONE); - binding.addToPlaylistButton.setVisibility(View.GONE); - - hideControls(0, 0); - binding.itemsListPanel.requestFocus(); - animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION, - AnimationType.SLIDE_AND_ALPHA); - - final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer - .getCurrentPosition()); - segmentAdapter.selectSegmentAt(adapterPosition); - binding.itemsList.scrollToPosition(adapterPosition); - } - - private void buildSegments() { - binding.itemsList.setAdapter(segmentAdapter); - binding.itemsList.setClickable(true); - binding.itemsList.setLongClickable(false); - - binding.itemsList.clearOnScrollListeners(); - if (itemTouchHelper != null) { - itemTouchHelper.attachToRecyclerView(null); - } - - getCurrentStreamInfo().ifPresent(segmentAdapter::setItems); - - binding.shuffleButton.setVisibility(View.GONE); - binding.repeatButton.setVisibility(View.GONE); - binding.addToPlaylistButton.setVisibility(View.GONE); - binding.itemsListClose.setOnClickListener(view -> closeItemsList()); - } - - public void closeItemsList() { - if (isQueueVisible || areSegmentsVisible) { - isQueueVisible = false; - areSegmentsVisible = false; - - if (itemTouchHelper != null) { - itemTouchHelper.attachToRecyclerView(null); - } - - animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION, - AnimationType.SLIDE_AND_ALPHA, 0, () -> { - // Even when queueLayout is GONE it receives touch events - // and ruins normal behavior of the app. This line fixes it - binding.itemsListPanel.setTranslationY( - -binding.itemsListPanel.getHeight() * 5); - }); - - // clear focus, otherwise a white rectangle remains on top of the player - binding.itemsListClose.clearFocus(); - binding.playPauseButton.requestFocus(); - } - } - - private OnScrollBelowItemsListener getQueueScrollListener() { - return new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(final RecyclerView recyclerView) { - if (playQueue != null && !playQueue.isComplete()) { - playQueue.fetch(); - } else if (binding != null) { - binding.itemsList.clearOnScrollListeners(); - } - } - }; - } - - private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { - return (item, seconds) -> { - segmentAdapter.selectSegment(item); - seekTo(seconds * 1000L); - triggerProgressUpdate(); - }; - } - - private int getNearestStreamSegmentPosition(final long playbackPosition) { - int nearestPosition = 0; - final List segments = getCurrentStreamInfo() - .map(StreamInfo::getStreamSegments) - .orElse(Collections.emptyList()); - - for (int i = 0; i < segments.size(); i++) { - if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) { - break; - } - nearestPosition++; - } - return Math.max(0, nearestPosition - 1); - } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new PlayQueueItemTouchCallback() { - @Override - public void onMove(final int sourceIndex, final int targetIndex) { - if (playQueue != null) { - playQueue.move(sourceIndex, targetIndex); - } - } - - @Override - public void onSwiped(final int index) { - if (index != -1) { - playQueue.remove(index); - } - } - }; - } - - private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { - return new PlayQueueItemBuilder.OnSelectedListener() { - @Override - public void selected(final PlayQueueItem item, final View view) { - selectQueueItem(item); - } - - @Override - public void held(final PlayQueueItem item, final View view) { - if (playQueue.indexOf(item) != -1) { - openPopupMenu(playQueue, item, view, true, - getParentActivity().getSupportFragmentManager(), context); - } - } - - @Override - public void onStartDrag(final PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) { - itemTouchHelper.startDrag(viewHolder); - } - } - }; + UIs.call(PlayerUi::onPlayQueueEdited); } @Override // own playback listener @@ -3373,279 +1886,21 @@ public final class Player implements @Nullable public VideoStream getSelectedVideoStream() { - return (selectedStreamIndex >= 0 && availableStreams != null - && availableStreams.size() > selectedStreamIndex) - ? availableStreams.get(selectedStreamIndex) : null; - } - - private void updateStreamRelatedViews() { - if (!getCurrentStreamInfo().isPresent()) { - return; - } - final StreamInfo info = getCurrentStreamInfo().get(); - - binding.qualityTextView.setVisibility(View.GONE); - binding.playbackSpeed.setVisibility(View.GONE); - - binding.playbackEndTime.setVisibility(View.GONE); - binding.playbackLiveSync.setVisibility(View.GONE); - - switch (info.getStreamType()) { - case AUDIO_STREAM: - case POST_LIVE_AUDIO_STREAM: - binding.surfaceView.setVisibility(View.GONE); - binding.endScreen.setVisibility(View.VISIBLE); - binding.playbackEndTime.setVisibility(View.VISIBLE); - break; - - case AUDIO_LIVE_STREAM: - binding.surfaceView.setVisibility(View.GONE); - binding.endScreen.setVisibility(View.VISIBLE); - binding.playbackLiveSync.setVisibility(View.VISIBLE); - break; - - case LIVE_STREAM: - binding.surfaceView.setVisibility(View.VISIBLE); - binding.endScreen.setVisibility(View.GONE); - binding.playbackLiveSync.setVisibility(View.VISIBLE); - break; - - case VIDEO_STREAM: - case POST_LIVE_STREAM: - if (currentMetadata == null - || !currentMetadata.getMaybeQuality().isPresent() - || (info.getVideoStreams().isEmpty() - && info.getVideoOnlyStreams().isEmpty())) { - break; - } - - availableStreams = currentMetadata.getMaybeQuality().get().getSortedVideoStreams(); - selectedStreamIndex = - currentMetadata.getMaybeQuality().get().getSelectedVideoStreamIndex(); - buildQualityMenu(); - - binding.qualityTextView.setVisibility(View.VISIBLE); - binding.surfaceView.setVisibility(View.VISIBLE); - default: - binding.endScreen.setVisibility(View.GONE); - binding.playbackEndTime.setVisibility(View.VISIBLE); - break; + @Nullable final MediaItemTag.Quality quality = Optional.ofNullable(currentMetadata) + .flatMap(MediaItemTag::getMaybeQuality) + .orElse(null); + if (quality == null) { + return null; } - buildPlaybackSpeedMenu(); - binding.playbackSpeed.setVisibility(View.VISIBLE); - } + final List availableStreams = quality.getSortedVideoStreams(); + final int selectedStreamIndex = quality.getSelectedVideoStreamIndex(); - private void updateQueueTime(final int currentTime) { - final int currentStream = playQueue.getIndex(); - int before = 0; - int after = 0; - - final List streams = playQueue.getStreams(); - final int nStreams = streams.size(); - - for (int i = 0; i < nStreams; i++) { - if (i < currentStream) { - before += streams.get(i).getDuration(); - } else { - after += streams.get(i).getDuration(); - } + if (selectedStreamIndex >= 0 && availableStreams.size() > selectedStreamIndex) { + return availableStreams.get(selectedStreamIndex); + } else { + return null; } - - before *= 1000; - after *= 1000; - - binding.itemsListHeaderDuration.setText( - String.format("%s/%s", - getTimeString(currentTime + before), - getTimeString(before + after) - )); - } - //endregion - - - - /*////////////////////////////////////////////////////////////////////////// - // Popup menus ("popup" means that they pop up, not that they belong to the popup player) - //////////////////////////////////////////////////////////////////////////*/ - //region Popup menus ("popup" means that they pop up, not that they belong to the popup player) - - private void buildQualityMenu() { - if (qualityPopupMenu == null) { - return; - } - qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY); - - for (int i = 0; i < availableStreams.size(); i++) { - final VideoStream videoStream = availableStreams.get(i); - qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat - .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution()); - } - if (getSelectedVideoStream() != null) { - binding.qualityTextView.setText(getSelectedVideoStream().getResolution()); - } - qualityPopupMenu.setOnMenuItemClickListener(this); - qualityPopupMenu.setOnDismissListener(this); - } - - private void buildPlaybackSpeedMenu() { - if (playbackSpeedPopupMenu == null) { - return; - } - playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED); - - for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { - playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE, - formatSpeed(PLAYBACK_SPEEDS[i])); - } - binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); - playbackSpeedPopupMenu.setOnMenuItemClickListener(this); - playbackSpeedPopupMenu.setOnDismissListener(this); - } - - private void buildCaptionMenu(@NonNull final List availableLanguages) { - if (captionPopupMenu == null) { - return; - } - captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION); - captionPopupMenu.setOnDismissListener(this); - - // Add option for turning off caption - final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, - 0, Menu.NONE, R.string.caption_none); - captionOffItem.setOnMenuItemClickListener(menuItem -> { - final int textRendererIndex = getCaptionRendererIndex(); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, true)); - } - prefs.edit().remove(context.getString(R.string.caption_user_set_key)).apply(); - return true; - }); - - // Add all available captions - for (int i = 0; i < availableLanguages.size(); i++) { - final String captionLanguage = availableLanguages.get(i); - final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, - i + 1, Menu.NONE, captionLanguage); - captionItem.setOnMenuItemClickListener(menuItem -> { - final int textRendererIndex = getCaptionRendererIndex(); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - // DefaultTrackSelector will select for text tracks in the following order. - // When multiple tracks share the same rank, a random track will be chosen. - // 1. ANY track exactly matching preferred language name - // 2. ANY track exactly matching preferred language stem - // 3. ROLE_FLAG_CAPTION track matching preferred language stem - // 4. ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND track matching preferred language stem - // This means if a caption track of preferred language is not available, - // then an auto-generated track of that language will be chosen automatically. - trackSelector.setParameters(trackSelector.buildUponParameters() - .setPreferredTextLanguages(captionLanguage, - PlayerHelper.captionLanguageStemOf(captionLanguage)) - .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) - .setRendererDisabled(textRendererIndex, false)); - prefs.edit().putString(context.getString(R.string.caption_user_set_key), - captionLanguage).apply(); - } - return true; - }); - } - - // apply caption language from previous user preference - final int textRendererIndex = getCaptionRendererIndex(); - if (textRendererIndex == RENDERER_UNAVAILABLE) { - return; - } - - // If user prefers to show no caption, then disable the renderer. - // Otherwise, DefaultTrackSelector may automatically find an available caption - // and display that. - final String userPreferredLanguage = - prefs.getString(context.getString(R.string.caption_user_set_key), null); - if (userPreferredLanguage == null) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, true)); - return; - } - - // Only set preferred language if it does not match the user preference, - // otherwise there might be an infinite cycle at onTextTracksChanged. - final List selectedPreferredLanguages = - trackSelector.getParameters().preferredTextLanguages; - if (!selectedPreferredLanguages.contains(userPreferredLanguage)) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setPreferredTextLanguages(userPreferredLanguage, - PlayerHelper.captionLanguageStemOf(userPreferredLanguage)) - .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) - .setRendererDisabled(textRendererIndex, false)); - } - } - - /** - * Called when an item of the quality selector or the playback speed selector is selected. - */ - @Override - public boolean onMenuItemClick(@NonNull final MenuItem menuItem) { - if (DEBUG) { - Log.d(TAG, "onMenuItemClick() called with: " - + "menuItem = [" + menuItem + "], " - + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); - } - - if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { - final int menuItemIndex = menuItem.getItemId(); - if (selectedStreamIndex == menuItemIndex || availableStreams == null - || availableStreams.size() <= menuItemIndex) { - return true; - } - - saveStreamProgressState(); //TODO added, check if good - final String newResolution = availableStreams.get(menuItemIndex).getResolution(); - setRecovery(); - setPlaybackQuality(newResolution); - reloadPlayQueueManager(); - - binding.qualityTextView.setText(menuItem.getTitle()); - return true; - } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { - final int speedIndex = menuItem.getItemId(); - final float speed = PLAYBACK_SPEEDS[speedIndex]; - - setPlaybackSpeed(speed); - binding.playbackSpeed.setText(formatSpeed(speed)); - } - - return false; - } - - /** - * Called when some popup menu is dismissed. - */ - @Override - public void onDismiss(@Nullable final PopupMenu menu) { - if (DEBUG) { - Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); - } - isSomePopupMenuVisible = false; //TODO check if this works - if (getSelectedVideoStream() != null) { - binding.qualityTextView.setText(getSelectedVideoStream().getResolution()); - } - if (isPlaying()) { - hideControls(DEFAULT_CONTROLS_DURATION, 0); - hideSystemUIIfNeeded(); - } - } - - private void onCaptionClicked() { - if (DEBUG) { - Log.d(TAG, "onCaptionClicked() called"); - } - captionPopupMenu.show(); - isSomePopupMenuVisible = true; - } - - private void setPlaybackQuality(@Nullable final String quality) { - videoResolver.setPlaybackQuality(quality); } //endregion @@ -3656,67 +1911,7 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Captions (text tracks) - private void setupSubtitleView() { - final float captionScale = PlayerHelper.getCaptionScale(context); - final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); - if (popupPlayerSelected()) { - final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; - binding.subtitleView.setFractionalTextSize( - SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); - } else { - 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.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT); - binding.subtitleView.setStyle(captionStyle); - } - - private void onTextTracksChanged(@NonNull final TracksInfo currentTrackInfo) { - if (binding == null) { - return; - } - - if (trackSelector.getCurrentMappedTrackInfo() == null - || !currentTrackInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_TEXT)) { - binding.captionTextView.setVisibility(View.GONE); - return; - } - - // Extract all loaded languages - final List textTracks = currentTrackInfo - .getTrackGroupInfos() - .stream() - .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getTrackType()) - .collect(Collectors.toList()); - final List availableLanguages = textTracks.stream() - .map(TracksInfo.TrackGroupInfo::getTrackGroup) - .filter(textTrack -> textTrack.length > 0) - .map(textTrack -> textTrack.getFormat(0).language) - .collect(Collectors.toList()); - - // Find selected text track - final Optional selectedTracks = textTracks.stream() - .filter(TracksInfo.TrackGroupInfo::isSelected) - .filter(info -> info.getTrackGroup().length >= 1) - .map(info -> info.getTrackGroup().getFormat(0)) - .findFirst(); - - // Build UI - buildCaptionMenu(availableLanguages); - if (trackSelector.getParameters().getRendererDisabled(getCaptionRendererIndex()) - || !selectedTracks.isPresent()) { - binding.captionTextView.setText(R.string.caption_none); - } else { - binding.captionTextView.setText(selectedTracks.get().language); - } - binding.captionTextView.setVisibility( - availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); - } - - private int getCaptionRendererIndex() { + public int getCaptionRendererIndex() { if (exoPlayerIsNull()) { return RENDERER_UNAVAILABLE; } @@ -3732,218 +1927,10 @@ public final class Player implements //endregion - /*////////////////////////////////////////////////////////////////////////// - // Click listeners + // Video size //////////////////////////////////////////////////////////////////////////*/ - //region Click listeners - - @Override - public void onClick(final View v) { - if (DEBUG) { - Log.d(TAG, "onClick() called with: v = [" + v + "]"); - } - if (v.getId() == binding.resizeTextView.getId()) { - onResizeClicked(); - } else if (v.getId() == binding.captionTextView.getId()) { - onCaptionClicked(); - } else if (v.getId() == binding.playbackLiveSync.getId()) { - seekToDefault(); - } else if (v.getId() == binding.playPauseButton.getId()) { - playPause(); - } else if (v.getId() == binding.playPreviousButton.getId()) { - playPrevious(); - } else if (v.getId() == binding.playNextButton.getId()) { - playNext(); - } else if (v.getId() == binding.moreOptionsButton.getId()) { - onMoreOptionsClicked(); - } else if (v.getId() == binding.share.getId()) { - ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime(), - currentItem.getThumbnailUrl()); - } else if (v.getId() == binding.playWithKodi.getId()) { - onPlayWithKodiClicked(); - } else if (v.getId() == binding.openInBrowser.getId()) { - onOpenInBrowserClicked(); - } else if (v.getId() == binding.fullScreenButton.getId()) { - setRecovery(); - NavigationHelper.playOnMainPlayer(context, playQueue, true); - return; - } else if (v.getId() == binding.screenRotationButton.getId()) { - // Only if it's not a vertical video or vertical video but in landscape with locked - // orientation a screen orientation can be changed automatically - if (!isVerticalVideo - || (service.isLandscape() && globalScreenOrientationLocked(context))) { - fragmentListener.onScreenRotationButtonClicked(); - } else { - toggleFullscreen(); - } - } else if (v.getId() == binding.switchMute.getId()) { - onMuteUnmuteButtonClicked(); - } else if (v.getId() == binding.playerCloseButton.getId()) { - context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); - } - - manageControlsAfterOnClick(v); - } - - /** - * Manages the controls after a click occurred on the player UI. - * @param v – The view that was clicked - */ - public void manageControlsAfterOnClick(@NonNull final View v) { - if (currentState == STATE_COMPLETED) { - return; - } - - controlsVisibilityHandler.removeCallbacksAndMessages(null); - showHideShadow(true, DEFAULT_CONTROLS_DURATION); - animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, - AnimationType.ALPHA, 0, () -> { - if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) { - if (v.getId() == binding.playPauseButton.getId() - // Hide controls in fullscreen immediately - || (v.getId() == binding.screenRotationButton.getId() - && isFullscreen)) { - hideControls(0, 0); - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - } - }); - } - - @Override - public boolean onLongClick(final View v) { - if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) { - fragmentListener.onMoreOptionsLongClicked(); - hideControls(0, 0); - hideSystemUIIfNeeded(); - } else if (v.getId() == binding.share.getId()) { - ShareUtils.copyToClipboard(context, getVideoUrlAtCurrentTime()); - } - return true; - } - - public boolean onKeyDown(final int keyCode) { - switch (keyCode) { - default: - break; - case KeyEvent.KEYCODE_SPACE: - if (isFullscreen) { - playPause(); - if (isPlaying()) { - hideControls(0, 0); - } - return true; - } - break; - case KeyEvent.KEYCODE_BACK: - if (DeviceUtils.isTv(context) && isControlsVisible()) { - hideControls(0, 0); - return true; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) - || isQueueVisible) { - // do not interfere with focus in playlist and play queue etc. - return false; - } - - if (currentState == Player.STATE_BLOCKED) { - return true; - } - - if (isControlsVisible()) { - hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); - } else { - binding.playPauseButton.requestFocus(); - showControlsThenHide(); - showSystemUIPartially(); - return true; - } - break; - } - - return false; - } - - private void onMoreOptionsClicked() { - if (DEBUG) { - Log.d(TAG, "onMoreOptionsClicked() called"); - } - - final boolean isMoreControlsVisible = - binding.secondaryControls.getVisibility() == View.VISIBLE; - - animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, - isMoreControlsVisible ? 0 : 180); - animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION, - AnimationType.SLIDE_AND_ALPHA, 0, () -> { - // Fix for a ripple effect on background drawable. - // When view returns from GONE state it takes more milliseconds than returning - // from INVISIBLE state. And the delay makes ripple background end to fast - if (isMoreControlsVisible) { - binding.secondaryControls.setVisibility(View.INVISIBLE); - } - }); - showControls(DEFAULT_CONTROLS_DURATION); - } - - private void onPlayWithKodiClicked() { - if (currentMetadata != null) { - pause(); - try { - NavigationHelper.playWithKore(context, Uri.parse(getVideoUrl())); - } catch (final Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtils.showInstallKoreDialog(getParentActivity()); - } - } - } - - private void onOpenInBrowserClicked() { - getCurrentStreamInfo() - .map(Info::getOriginalUrl) - .ifPresent(originalUrl -> ShareUtils.openUrlInBrowser( - Objects.requireNonNull(getParentActivity()), originalUrl)); - } - //endregion - - - - /*////////////////////////////////////////////////////////////////////////// - // Video size, resize, orientation, fullscreen - //////////////////////////////////////////////////////////////////////////*/ - //region Video size, resize, orientation, fullscreen - - private void setupScreenRotationButton() { - binding.screenRotationButton.setVisibility(videoPlayerSelected() - && (globalScreenOrientationLocked(context) || isVerticalVideo - || DeviceUtils.isTablet(context)) - ? View.VISIBLE : View.GONE); - binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context, - isFullscreen ? R.drawable.ic_fullscreen_exit - : R.drawable.ic_fullscreen)); - } - - private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - binding.surfaceView.setResizeMode(resizeMode); - binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode)); - } - - void onResizeClicked() { - if (binding != null) { - setResizeMode(nextResizeModeAndSaveToPrefs(this, binding.surfaceView.getResizeMode())); - } - } - + //region Video size @Override // exoplayer listener public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { if (DEBUG) { @@ -3954,137 +1941,11 @@ public final class Player implements + "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]"); } - binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height); - isVerticalVideo = videoSize.width < videoSize.height; - - if (globalScreenOrientationLocked(context) - && isFullscreen - && service.isLandscape() == isVerticalVideo - && !DeviceUtils.isTv(context) - && !DeviceUtils.isTablet(context) - && fragmentListener != null) { - // set correct orientation - fragmentListener.onScreenRotationButtonClicked(); - } - - setupScreenRotationButton(); - } - - public void toggleFullscreen() { - if (DEBUG) { - Log.d(TAG, "toggleFullscreen() called"); - } - if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) { - return; - } - - isFullscreen = !isFullscreen; - if (!isFullscreen) { - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait (open vertical video to reproduce) - binding.playbackControlRoot.setPadding(0, 0, 0, 0); - } else { - // Android needs tens milliseconds to send new insets but a user is able to see - // how controls changes it's position from `0` to `nav bar height` padding. - // So just hide the controls to hide this visual inconsistency - hideControls(0, 0); - } - fragmentListener.onFullscreenStateChanged(isFullscreen); - - if (isFullscreen) { - binding.titleTextView.setVisibility(View.VISIBLE); - binding.channelTextView.setVisibility(View.VISIBLE); - binding.playerCloseButton.setVisibility(View.GONE); - } else { - binding.titleTextView.setVisibility(View.GONE); - binding.channelTextView.setVisibility(View.GONE); - binding.playerCloseButton.setVisibility( - videoPlayerSelected() ? View.VISIBLE : View.GONE); - } - setupScreenRotationButton(); - } - - public void checkLandscape() { - final AppCompatActivity parent = getParentActivity(); - final boolean videoInLandscapeButNotInFullscreen = - service.isLandscape() && !isFullscreen && videoPlayerSelected() && !isAudioOnly; - - final boolean notPaused = currentState != STATE_COMPLETED && currentState != STATE_PAUSED; - if (parent != null - && videoInLandscapeButNotInFullscreen - && notPaused - && !DeviceUtils.isTablet(context)) { - toggleFullscreen(); - } + UIs.call(playerUi -> playerUi.onVideoSizeChanged(videoSize)); } //endregion - - /*////////////////////////////////////////////////////////////////////////// - // Gestures - //////////////////////////////////////////////////////////////////////////*/ - //region Gestures - - @SuppressWarnings("checkstyle:ParameterNumber") - private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, - final int ol, final int ot, final int or, final int ob) { - if (l != ol || t != ot || r != or || b != ob) { - // Use smaller value to be consistent between screen orientations - // (and to make usage easier) - final int width = r - l; - final int height = b - t; - final int min = Math.min(width, height); - maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); - - if (DEBUG) { - Log.d(TAG, "maxGestureLength = " + maxGestureLength); - } - - binding.volumeProgressBar.setMax(maxGestureLength); - binding.brightnessProgressBar.setMax(maxGestureLength); - - setInitialGestureValues(); - binding.itemsListPanel.getLayoutParams().height - = height - binding.itemsListPanel.getTop(); - } - } - - private void setInitialGestureValues() { - if (audioReactor != null) { - final float currentVolumeNormalized = - (float) audioReactor.getVolume() / audioReactor.getMaxVolume(); - binding.volumeProgressBar.setProgress( - (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); - } - } - - private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft() - + closeOverlayBinding.closeButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop() - + closeOverlayBinding.closeButton.getHeight() / 2; - - final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); - final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) - + Math.pow(closeOverlayButtonY - fingerY, 2)); - } - - private float getClosingRadius() { - final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2; - // 20% wider than the button itself - return buttonRadius * 1.2f; - } - - public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) { - return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); - } - //endregion - - - /*////////////////////////////////////////////////////////////////////////// // Activity / fragment binding //////////////////////////////////////////////////////////////////////////*/ @@ -4092,13 +1953,7 @@ public final class Player implements public void setFragmentListener(final PlayerServiceEventListener listener) { fragmentListener = listener; - fragmentIsVisible = true; - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait - if (!isFullscreen) { - binding.playbackControlRoot.setPadding(0, 0, 0, 0); - } - binding.itemsListPanel.setPadding(0, 0, 0, 0); + UIs.call(PlayerUi::onFragmentListenerSet); notifyQueueUpdateToListeners(); notifyMetadataUpdateToListeners(); notifyPlaybackUpdateToListeners(); @@ -4136,28 +1991,6 @@ public final class Player implements } } - /** - * This will be called when a user goes to another app/activity, turns off a screen. - * We don't want to interrupt playback and don't want to see notification so - * next lines of code will enable audio-only playback only if needed - */ - private void onFragmentStopped() { - if (videoPlayerSelected() && (isPlaying() || isLoading())) { - switch (getMinimizeOnExitAction(context)) { - case MINIMIZE_ON_EXIT_MODE_BACKGROUND: - useVideoSource(false); - break; - case MINIMIZE_ON_EXIT_MODE_POPUP: - setRecovery(); - NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); - break; - case MINIMIZE_ON_EXIT_MODE_NONE: default: - pause(); - break; - } - } - } - private void notifyQueueUpdateToListeners() { if (fragmentListener != null && playQueue != null) { fragmentListener.onQueueUpdate(playQueue); @@ -4200,27 +2033,12 @@ public final class Player implements } } - @Nullable - public AppCompatActivity getParentActivity() { - // ! instanceof ViewGroup means that view was added via windowManager for Popup - if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) { - return null; - } - - return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); - } - - private void useVideoSource(final boolean videoEnabled) { + public void useVideoSource(final boolean videoEnabled) { if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) { return; } isAudioOnly = !videoEnabled; - // When a user returns from background, controls could be hidden but SystemUI will be shown - // 100%. Hide it. - if (!isAudioOnly && !isControlsVisible()) { - hideSystemUIIfNeeded(); - } // The current metadata may be null sometimes (for e.g. when using an unstable connection // in livestreams) so we will be not able to execute the block below. @@ -4249,20 +2067,12 @@ public final class Player implements return; } - final DefaultTrackSelector.ParametersBuilder parametersBuilder = + final DefaultTrackSelector.Parameters.Builder parametersBuilder = trackSelector.buildUponParameters(); - if (videoEnabled) { - // Enable again the video track and the subtitles, if there is one selected - parametersBuilder.setDisabledTrackTypes(Collections.emptySet()); - } else { - // Disable the video track and the ability to select subtitles - // Use an ArraySet because we can't use Set.of() on all supported APIs by the app - final ArraySet disabledTracks = new ArraySet<>(); - disabledTracks.add(C.TRACK_TYPE_TEXT); - disabledTracks.add(C.TRACK_TYPE_VIDEO); - parametersBuilder.setDisabledTrackTypes(disabledTracks); - } + // Enable/disable the video track and the ability to select subtitles + parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !videoEnabled); + parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, !videoEnabled); trackSelector.setParameters(parametersBuilder); } @@ -4340,7 +2150,7 @@ public final class Player implements //////////////////////////////////////////////////////////////////////////*/ //region Getters - private Optional getCurrentStreamInfo() { + public Optional getCurrentStreamInfo() { return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo); } @@ -4352,6 +2162,10 @@ public final class Player implements return simpleExoPlayer == null; } + public ExoPlayer getExoPlayer() { + return simpleExoPlayer; + } + public boolean isStopped() { return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE; } @@ -4364,7 +2178,7 @@ public final class Player implements return !exoPlayerIsNull() && simpleExoPlayer.getPlayWhenReady(); } - private boolean isLoading() { + public boolean isLoading() { return !exoPlayerIsNull() && simpleExoPlayer.isLoading(); } @@ -4380,6 +2194,10 @@ public final class Player implements } } + public void setPlaybackQuality(@Nullable final String quality) { + videoResolver.setPlaybackQuality(quality); + } + @NonNull public Context getContext() { @@ -4391,10 +2209,6 @@ public final class Player implements return prefs; } - public MediaSessionManager getMediaSessionManager() { - return mediaSessionManager; - } - public PlayerType getPlayerType() { return playerType; @@ -4405,7 +2219,7 @@ public final class Player implements } public boolean videoPlayerSelected() { - return playerType == PlayerType.VIDEO; + return playerType == PlayerType.MAIN; } public boolean popupPlayerSelected() { @@ -4422,157 +2236,41 @@ public final class Player implements return audioReactor; } - public GestureDetector getGestureDetector() { - return gestureDetector; + public PlayerService getService() { + return service; } - public boolean isFullscreen() { - return isFullscreen; + public boolean isAudioOnly() { + return isAudioOnly; } - public boolean isVerticalVideo() { - return isVerticalVideo; - } - - public boolean isPopupClosing() { - return isPopupClosing; - } - - - public boolean isSomePopupMenuVisible() { - return isSomePopupMenuVisible; - } - - public void setSomePopupMenuVisible(final boolean somePopupMenuVisible) { - isSomePopupMenuVisible = somePopupMenuVisible; - } - - public ImageButton getPlayPauseButton() { - return binding.playPauseButton; - } - - public View getClosingOverlayView() { - return binding.closingOverlay; - } - - public ProgressBar getVolumeProgressBar() { - return binding.volumeProgressBar; - } - - public ProgressBar getBrightnessProgressBar() { - return binding.brightnessProgressBar; - } - - public int getMaxGestureLength() { - return maxGestureLength; - } - - public ImageView getVolumeImageView() { - return binding.volumeImageView; - } - - public RelativeLayout getVolumeRelativeLayout() { - return binding.volumeRelativeLayout; - } - - public ImageView getBrightnessImageView() { - return binding.brightnessImageView; - } - - public RelativeLayout getBrightnessRelativeLayout() { - return binding.brightnessRelativeLayout; - } - - public FloatingActionButton getCloseOverlayButton() { - return closeOverlayBinding.closeButton; - } - - public View getLoadingPanel() { - return binding.loadingPanel; - } - - public TextView getCurrentDisplaySeek() { - return binding.currentDisplaySeek; - } - - public PlayerFastSeekOverlay getFastSeekOverlay() { - return binding.fastSeekOverlay; + @NonNull + public DefaultTrackSelector getTrackSelector() { + return trackSelector; } @Nullable - public WindowManager.LayoutParams getPopupLayoutParams() { - return popupLayoutParams; + public MediaItemTag getCurrentMetadata() { + return currentMetadata; } @Nullable - public WindowManager getWindowManager() { - return windowManager; + public PlayQueueItem getCurrentItem() { + return currentItem; } - public float getScreenWidth() { - return screenWidth; + public Optional getFragmentListener() { + return Optional.ofNullable(fragmentListener); } - public float getScreenHeight() { - return screenHeight; + /** + * @return the user interfaces connected with the player + */ + @SuppressWarnings("MethodName") // keep the unusual method name + public PlayerUiList UIs() { + return UIs; } - public View getRootView() { - return binding.getRoot(); - } - - public ExpandableSurfaceView getSurfaceView() { - return binding.surfaceView; - } - - public PlayQueueAdapter getPlayQueueAdapter() { - return playQueueAdapter; - } - - public PlayerBinding getBinding() { - return binding; - } - - //endregion - - - /*////////////////////////////////////////////////////////////////////////// - // SurfaceHolderCallback helpers - //////////////////////////////////////////////////////////////////////////*/ - //region SurfaceHolderCallback helpers - - private void setupVideoSurface() { - // make sure there is nothing left over from previous calls - cleanupVideoSurface(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 - surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer); - binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); - final Surface surface = binding.surfaceView.getHolder().getSurface(); - // ensure player is using an unreleased surface, which the surfaceView might not be - // when starting playback on background or during player switching - if (surface.isValid()) { - // initially set the surface manually otherwise - // onRenderedFirstFrame() will not be called - simpleExoPlayer.setVideoSurface(surface); - } - } else { - simpleExoPlayer.setVideoSurfaceView(binding.surfaceView); - } - } - - private void cleanupVideoSurface() { - // Only for API >= 23 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) { - if (binding != null) { - binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); - } - surfaceHolderCallback.release(); - surfaceHolderCallback = null; - } - } - //endregion - /** * Get the video renderer index of the current playing stream. * @@ -4600,4 +2298,5 @@ public final class Player implements // No video renderer index with at least one track found: return unavailable index .orElse(RENDERER_UNAVAILABLE); } + //endregion } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java new file mode 100644 index 000000000..33b024e3d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Mauricio Colli + * Part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +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.IBinder; +import android.util.Log; + +import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; +import org.schabi.newpipe.util.ThemeHelper; + + +/** + * One service for all players. + */ +public final class PlayerService extends Service { + private static final String TAG = PlayerService.class.getSimpleName(); + private static final boolean DEBUG = Player.DEBUG; + + private Player player; + + private final IBinder mBinder = new PlayerService.LocalBinder(); + + + /*////////////////////////////////////////////////////////////////////////// + // Service's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate() { + if (DEBUG) { + Log.d(TAG, "onCreate() called"); + } + assureCorrectAppLanguage(this); + ThemeHelper.setTheme(this); + + player = new Player(this); + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (DEBUG) { + Log.d(TAG, "onStartCommand() called with: intent = [" + intent + + "], flags = [" + flags + "], startId = [" + startId + "]"); + } + + if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) + && player.getPlayQueue() == null) { + // No need to process media button's actions if the player is not working, otherwise the + // player service would strangely start with nothing to play + return START_NOT_STICKY; + } + + player.handleIntent(intent); + player.UIs().get(MediaSessionPlayerUi.class) + .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); + + return START_NOT_STICKY; + } + + public void stopForImmediateReusing() { + if (DEBUG) { + Log.d(TAG, "stopForImmediateReusing() called"); + } + + if (!player.exoPlayerIsNull()) { + player.saveWasPlaying(); + + // Releases wifi & cpu, disables keepScreenOn, etc. + // We can't just pause the player here because it will make transition + // from one stream to a new stream not smooth + player.smoothStopForImmediateReusing(); + } + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + if (!player.videoPlayerSelected()) { + return; + } + onDestroy(); + // Unload from memory completely + Runtime.getRuntime().halt(0); + } + + @Override + public void onDestroy() { + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } + cleanup(); + } + + private void cleanup() { + if (player != null) { + player.destroy(); + player = null; + } + } + + public void stopService() { + cleanup(); + stopSelf(); + } + + @Override + protected void attachBaseContext(final Context base) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); + } + + @Override + public IBinder onBind(final Intent intent) { + return mBinder; + } + + public class LocalBinder extends Binder { + + public PlayerService getService() { + return PlayerService.this; + } + + public Player getPlayer() { + return PlayerService.this.player; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java deleted file mode 100644 index 5c28c6c7b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.schabi.newpipe.player; - -import android.os.Binder; - -import androidx.annotation.NonNull; - -class PlayerServiceBinder extends Binder { - private final Player player; - - PlayerServiceBinder(@NonNull final Player player) { - this.player = player; - } - - Player getPlayerInstance() { - return player; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java deleted file mode 100644 index af875a32b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.schabi.newpipe.player; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.player.playqueue.PlayQueue; - -import java.io.Serializable; - -public class PlayerState implements Serializable { - - @NonNull - private final PlayQueue playQueue; - private final int repeatMode; - private final float playbackSpeed; - private final float playbackPitch; - @Nullable - private final String playbackQuality; - private final boolean playbackSkipSilence; - private final boolean wasPlaying; - - PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, - final float playbackSpeed, final float playbackPitch, - final boolean playbackSkipSilence, final boolean wasPlaying) { - this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, - playbackSkipSilence, wasPlaying); - } - - PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode, - final float playbackSpeed, final float playbackPitch, - @Nullable final String playbackQuality, final boolean playbackSkipSilence, - final boolean wasPlaying) { - this.playQueue = playQueue; - this.repeatMode = repeatMode; - this.playbackSpeed = playbackSpeed; - this.playbackPitch = playbackPitch; - this.playbackQuality = playbackQuality; - this.playbackSkipSilence = playbackSkipSilence; - this.wasPlaying = wasPlaying; - } - - /*////////////////////////////////////////////////////////////////////////// - // Serdes - //////////////////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////////////////// - // Getters - //////////////////////////////////////////////////////////////////////////*/ - - @NonNull - public PlayQueue getPlayQueue() { - return playQueue; - } - - public int getRepeatMode() { - return repeatMode; - } - - public float getPlaybackSpeed() { - return playbackSpeed; - } - - public float getPlaybackPitch() { - return playbackPitch; - } - - @Nullable - public String getPlaybackQuality() { - return playbackQuality; - } - - public boolean isPlaybackSkipSilence() { - return playbackSkipSilence; - } - - public boolean wasPlaying() { - return wasPlaying; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java new file mode 100644 index 000000000..171a70395 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java @@ -0,0 +1,32 @@ +package org.schabi.newpipe.player; + +import static org.schabi.newpipe.player.Player.PLAYER_TYPE; + +import android.content.Intent; + +public enum PlayerType { + MAIN, + AUDIO, + POPUP; + + /** + * @return an integer representing this {@link PlayerType}, to be used to save it in intents + * @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type + * integers from an intent + */ + public int valueForIntent() { + return ordinal(); + } + + /** + * @param intent the intent to retrieve a player type from + * @return the player type integer retrieved from the intent, converted back into a {@link + * PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the + * intent + * @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer + * @see #valueForIntent() Use valueForIntent() to obtain valid player type integers + */ + public static PlayerType retrieveFromIntent(final Intent intent) { + return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())]; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java index c9abe65f6..cf1f03b45 100644 --- a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java @@ -1,5 +1,5 @@ /* - * Based on ExoPlayer's DefaultHttpDataSource, version 2.17.1. + * Based on ExoPlayer's DefaultHttpDataSource, version 2.18.1. * * Original source code copyright (C) 2016 The Android Open Source Project, licensed under the * Apache License, Version 2.0. diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt deleted file mode 100644 index c89eabb47..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt +++ /dev/null @@ -1,520 +0,0 @@ -package org.schabi.newpipe.player.event - -import android.content.Context -import android.os.Handler -import android.util.Log -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import org.schabi.newpipe.ktx.animate -import org.schabi.newpipe.player.MainPlayer -import org.schabi.newpipe.player.Player -import org.schabi.newpipe.player.helper.PlayerHelper -import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs -import kotlin.math.abs -import kotlin.math.hypot -import kotlin.math.max -import kotlin.math.min - -/** - * Base gesture handling for [Player] - * - * This class contains the logic for the player gestures like View preparations - * and provides some abstract methods to make it easier separating the logic from the UI. - */ -abstract class BasePlayerGestureListener( - @JvmField - protected val player: Player, - @JvmField - protected val service: MainPlayer -) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { - - // /////////////////////////////////////////////////////////////////// - // Abstract methods for VIDEO and POPUP - // /////////////////////////////////////////////////////////////////// - - abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion) - - abstract fun onSingleTap(playerType: MainPlayer.PlayerType) - - abstract fun onScroll( - playerType: MainPlayer.PlayerType, - portion: DisplayPortion, - initialEvent: MotionEvent, - movingEvent: MotionEvent, - distanceX: Float, - distanceY: Float - ) - - abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent) - - // /////////////////////////////////////////////////////////////////// - // Abstract methods for POPUP (exclusive) - // /////////////////////////////////////////////////////////////////// - - abstract fun onPopupResizingStart() - - abstract fun onPopupResizingEnd() - - private var initialPopupX: Int = -1 - private var initialPopupY: Int = -1 - - private var isMovingInMain = false - private var isMovingInPopup = false - private var isResizing = false - - private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity() - - // [popup] initial coordinates and distance between fingers - private var initPointerDistance = -1.0 - private var initFirstPointerX = -1f - private var initFirstPointerY = -1f - private var initSecPointerX = -1f - private var initSecPointerY = -1f - - // /////////////////////////////////////////////////////////////////// - // onTouch implementation - // /////////////////////////////////////////////////////////////////// - - override fun onTouch(v: View, event: MotionEvent): Boolean { - return if (player.popupPlayerSelected()) { - onTouchInPopup(v, event) - } else { - onTouchInMain(v, event) - } - } - - private fun onTouchInMain(v: View, event: MotionEvent): Boolean { - player.gestureDetector.onTouchEvent(event) - if (event.action == MotionEvent.ACTION_UP && isMovingInMain) { - isMovingInMain = false - onScrollEnd(MainPlayer.PlayerType.VIDEO, event) - } - return when (event.action) { - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { - v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen) - true - } - MotionEvent.ACTION_UP -> { - v.parent.requestDisallowInterceptTouchEvent(false) - false - } - else -> true - } - } - - private fun onTouchInPopup(v: View, event: MotionEvent): Boolean { - player.gestureDetector.onTouchEvent(event) - if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") - } - onPopupResizingStart() - - // record coordinates of fingers - initFirstPointerX = event.getX(0) - initFirstPointerY = event.getY(0) - initSecPointerX = event.getX(1) - initSecPointerY = event.getY(1) - // record distance between fingers - initPointerDistance = hypot( - initFirstPointerX - initSecPointerX.toDouble(), - initFirstPointerY - initSecPointerY.toDouble() - ) - - isResizing = true - } - if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { - if (DEBUG) { - Log.d( - TAG, - "onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" + - "[${event.rawX}, ${event.rawY}]" - ) - } - return handleMultiDrag(event) - } - if (event.action == MotionEvent.ACTION_UP) { - if (DEBUG) { - Log.d( - TAG, - "onTouch() ACTION_UP > v = [$v], e1.getRaw =" + - " [${event.rawX}, ${event.rawY}]" - ) - } - if (isMovingInPopup) { - isMovingInPopup = false - onScrollEnd(MainPlayer.PlayerType.POPUP, event) - } - if (isResizing) { - isResizing = false - - initPointerDistance = (-1).toDouble() - initFirstPointerX = (-1).toFloat() - initFirstPointerY = (-1).toFloat() - initSecPointerX = (-1).toFloat() - initSecPointerY = (-1).toFloat() - - onPopupResizingEnd() - player.changeState(player.currentState) - } - if (!player.isPopupClosing) { - savePopupPositionAndSizeToPrefs(player) - } - } - - v.performClick() - return true - } - - private fun handleMultiDrag(event: MotionEvent): Boolean { - if (initPointerDistance != -1.0 && event.pointerCount == 2) { - // get the movements of the fingers - val firstPointerMove = hypot( - event.getX(0) - initFirstPointerX.toDouble(), - event.getY(0) - initFirstPointerY.toDouble() - ) - val secPointerMove = hypot( - event.getX(1) - initSecPointerX.toDouble(), - event.getY(1) - initSecPointerY.toDouble() - ) - - // minimum threshold beyond which pinch gesture will work - val minimumMove = ViewConfiguration.get(service).scaledTouchSlop - - if (max(firstPointerMove, secPointerMove) > minimumMove) { - // calculate current distance between the pointers - val currentPointerDistance = hypot( - event.getX(0) - event.getX(1).toDouble(), - event.getY(0) - event.getY(1).toDouble() - ) - - val popupWidth = player.popupLayoutParams!!.width.toDouble() - // change co-ordinates of popup so the center stays at the same position - val newWidth = popupWidth * currentPointerDistance / initPointerDistance - initPointerDistance = currentPointerDistance - player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt() - - player.checkPopupPositionBounds() - player.updateScreenSize() - player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt()) - return true - } - } - return false - } - - // /////////////////////////////////////////////////////////////////// - // Simple gestures - // /////////////////////////////////////////////////////////////////// - - override fun onDown(e: MotionEvent): Boolean { - if (DEBUG) - Log.d(TAG, "onDown called with e = [$e]") - - if (isDoubleTapping && isDoubleTapEnabled) { - doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e)) - return true - } - - return if (player.popupPlayerSelected()) - onDownInPopup(e) - else - true - } - - private fun onDownInPopup(e: MotionEvent): Boolean { - // Fix popup position when the user touch it, it may have the wrong one - // because the soft input is visible (the draggable area is currently resized). - player.updateScreenSize() - player.checkPopupPositionBounds() - player.popupLayoutParams?.let { - initialPopupX = it.x - initialPopupY = it.y - } - return super.onDown(e) - } - - override fun onDoubleTap(e: MotionEvent): Boolean { - if (DEBUG) - Log.d(TAG, "onDoubleTap called with e = [$e]") - - onDoubleTap(e, getDisplayPortion(e)) - return true - } - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - if (DEBUG) - Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") - - if (isDoubleTapping) - return true - - if (player.popupPlayerSelected()) { - if (player.exoPlayerIsNull()) - return false - - onSingleTap(MainPlayer.PlayerType.POPUP) - return true - } else { - super.onSingleTapConfirmed(e) - if (player.currentState == Player.STATE_BLOCKED) - return true - - onSingleTap(MainPlayer.PlayerType.VIDEO) - } - return true - } - - override fun onLongPress(e: MotionEvent?) { - if (player.popupPlayerSelected()) { - player.updateScreenSize() - player.checkPopupPositionBounds() - player.changePopupSize(player.screenWidth.toInt()) - } - } - - override fun onScroll( - initialEvent: MotionEvent, - movingEvent: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - return if (player.popupPlayerSelected()) { - onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY) - } else { - onScrollInMain(initialEvent, movingEvent, distanceX, distanceY) - } - } - - override fun onFling( - e1: MotionEvent?, - e2: MotionEvent?, - velocityX: Float, - velocityY: Float - ): Boolean { - return if (player.popupPlayerSelected()) { - val absVelocityX = abs(velocityX) - val absVelocityY = abs(velocityY) - if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) { - if (absVelocityX > tossFlingVelocity) { - player.popupLayoutParams!!.x = velocityX.toInt() - } - if (absVelocityY > tossFlingVelocity) { - player.popupLayoutParams!!.y = velocityY.toInt() - } - player.checkPopupPositionBounds() - player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) - return true - } - return false - } else { - true - } - } - - private fun onScrollInMain( - initialEvent: MotionEvent, - movingEvent: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - - if (!player.isFullscreen) { - return false - } - - val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) - val isTouchingNavigationBar: Boolean = - initialEvent.y > (player.rootView.height - getNavigationBarHeight(service)) - if (isTouchingStatusBar || isTouchingNavigationBar) { - return false - } - - val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD - if ( - !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || - player.currentState == Player.STATE_COMPLETED - ) { - return false - } - - isMovingInMain = true - - onScroll( - MainPlayer.PlayerType.VIDEO, - getDisplayHalfPortion(initialEvent), - initialEvent, - movingEvent, - distanceX, - distanceY - ) - - return true - } - - private fun onScrollInPopup( - initialEvent: MotionEvent, - movingEvent: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - - if (isResizing) { - return super.onScroll(initialEvent, movingEvent, distanceX, distanceY) - } - - if (!isMovingInPopup) { - player.closeOverlayButton.animate(true, 200) - } - - isMovingInPopup = true - - val diffX: Float = (movingEvent.rawX - initialEvent.rawX) - var posX: Float = (initialPopupX + diffX) - val diffY: Float = (movingEvent.rawY - initialEvent.rawY) - var posY: Float = (initialPopupY + diffY) - - if (posX > player.screenWidth - player.popupLayoutParams!!.width) { - posX = (player.screenWidth - player.popupLayoutParams!!.width) - } else if (posX < 0) { - posX = 0f - } - - if (posY > player.screenHeight - player.popupLayoutParams!!.height) { - posY = (player.screenHeight - player.popupLayoutParams!!.height) - } else if (posY < 0) { - posY = 0f - } - - player.popupLayoutParams!!.x = posX.toInt() - player.popupLayoutParams!!.y = posY.toInt() - - onScroll( - MainPlayer.PlayerType.POPUP, - getDisplayHalfPortion(initialEvent), - initialEvent, - movingEvent, - distanceX, - distanceY - ) - - player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) - return true - } - - // /////////////////////////////////////////////////////////////////// - // Multi double tapping - // /////////////////////////////////////////////////////////////////// - - var doubleTapControls: DoubleTapListener? = null - private set - - private val isDoubleTapEnabled: Boolean - get() = doubleTapDelay > 0 - - var isDoubleTapping = false - private set - - fun doubleTapControls(listener: DoubleTapListener) = apply { - doubleTapControls = listener - } - - private var doubleTapDelay = DOUBLE_TAP_DELAY - private val doubleTapHandler: Handler = Handler() - private val doubleTapRunnable = Runnable { - if (DEBUG) - Log.d(TAG, "doubleTapRunnable called") - - isDoubleTapping = false - doubleTapControls?.onDoubleTapFinished() - } - - fun startMultiDoubleTap(e: MotionEvent) { - if (!isDoubleTapping) { - if (DEBUG) - Log.d(TAG, "startMultiDoubleTap called with e = [$e]") - - keepInDoubleTapMode() - doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e)) - } - } - - fun keepInDoubleTapMode() { - if (DEBUG) - Log.d(TAG, "keepInDoubleTapMode called") - - isDoubleTapping = true - doubleTapHandler.removeCallbacks(doubleTapRunnable) - doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay) - } - - fun endMultiDoubleTap() { - if (DEBUG) - Log.d(TAG, "endMultiDoubleTap called") - - isDoubleTapping = false - doubleTapHandler.removeCallbacks(doubleTapRunnable) - doubleTapControls?.onDoubleTapFinished() - } - - // /////////////////////////////////////////////////////////////////// - // Utils - // /////////////////////////////////////////////////////////////////// - - private fun getDisplayPortion(e: MotionEvent): DisplayPortion { - return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) { - when { - e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT - e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT - else -> DisplayPortion.MIDDLE - } - } else /* MainPlayer.PlayerType.VIDEO */ { - when { - e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT - e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT - else -> DisplayPortion.MIDDLE - } - } - } - - // Currently needed for scrolling since there is no action more the middle portion - private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { - return if (player.playerType == MainPlayer.PlayerType.POPUP) { - when { - e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF - else -> DisplayPortion.RIGHT_HALF - } - } else /* MainPlayer.PlayerType.VIDEO */ { - when { - e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF - else -> DisplayPortion.RIGHT_HALF - } - } - } - - private fun getNavigationBarHeight(context: Context): Int { - val resId = context.resources - .getIdentifier("navigation_bar_height", "dimen", "android") - return if (resId > 0) { - context.resources.getDimensionPixelSize(resId) - } else 0 - } - - private fun getStatusBarHeight(context: Context): Int { - val resId = context.resources - .getIdentifier("status_bar_height", "dimen", "android") - return if (resId > 0) { - context.resources.getDimensionPixelSize(resId) - } else 0 - } - - companion object { - private const val TAG = "BasePlayerGestListener" - private val DEBUG = Player.DEBUG - - private const val DOUBLE_TAP_DELAY = 550L - private const val MOVEMENT_THRESHOLD = 40 - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt deleted file mode 100644 index 84cfb9b8d..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.player.event - -interface DoubleTapListener { - fun onDoubleTapStarted(portion: DisplayPortion) {} - fun onDoubleTapProgressDown(portion: DisplayPortion) {} - fun onDoubleTapFinished() {} -} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index b5520e8be..84bd9d277 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.player.event; - import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.extractor.stream.StreamInfo; diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java deleted file mode 100644 index a7fb40c47..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ /dev/null @@ -1,256 +0,0 @@ -package org.schabi.newpipe.player.event; - -import static org.schabi.newpipe.ktx.AnimationType.ALPHA; -import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.player.Player.STATE_PLAYING; - -import android.app.Activity; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.ProgressBar; - -import androidx.annotation.NonNull; -import androidx.appcompat.content.res.AppCompatResources; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.Player; -import org.schabi.newpipe.player.helper.PlayerHelper; - -/** - * GestureListener for the player - * - * While {@link BasePlayerGestureListener} contains the logic behind the single gestures - * this class focuses on the visual aspect like hiding and showing the controls or changing - * volume/brightness during scrolling for specific events. - */ -public class PlayerGestureListener - extends BasePlayerGestureListener - implements View.OnTouchListener { - private static final String TAG = PlayerGestureListener.class.getSimpleName(); - private static final boolean DEBUG = MainActivity.DEBUG; - - private final int maxVolume; - - public PlayerGestureListener(final Player player, final MainPlayer service) { - super(player, service); - maxVolume = player.getAudioReactor().getMaxVolume(); - } - - @Override - public void onDoubleTap(@NonNull final MotionEvent event, - @NonNull final DisplayPortion portion) { - if (DEBUG) { - Log.d(TAG, "onDoubleTap called with playerType = [" - + player.getPlayerType() + "], portion = [" + portion + "]"); - } - if (player.isSomePopupMenuVisible()) { - player.hideControls(0, 0); - } - - if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) { - startMultiDoubleTap(event); - } else if (portion == DisplayPortion.MIDDLE) { - player.playPause(); - } - } - - @Override - public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) { - if (DEBUG) { - Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]"); - } - - if (player.isControlsVisible()) { - player.hideControls(150, 0); - return; - } - // -- Controls are not visible -- - - // When player is completed show controls and don't hide them later - if (player.getCurrentState() == Player.STATE_COMPLETED) { - player.showControls(0); - } else { - player.showControlsThenHide(); - } - } - - @Override - public void onScroll(@NonNull final MainPlayer.PlayerType playerType, - @NonNull final DisplayPortion portion, - @NonNull final MotionEvent initialEvent, - @NonNull final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if (DEBUG) { - Log.d(TAG, "onScroll called with playerType = [" - + player.getPlayerType() + "], portion = [" + portion + "]"); - } - if (playerType == MainPlayer.PlayerType.VIDEO) { - - // -- Brightness and Volume control -- - final boolean isBrightnessGestureEnabled = - PlayerHelper.isBrightnessGestureEnabled(service); - final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service); - - if (isBrightnessGestureEnabled && isVolumeGestureEnabled) { - if (portion == DisplayPortion.LEFT_HALF) { - onScrollMainBrightness(distanceX, distanceY); - - } else /* DisplayPortion.RIGHT_HALF */ { - onScrollMainVolume(distanceX, distanceY); - } - } else if (isBrightnessGestureEnabled) { - onScrollMainBrightness(distanceX, distanceY); - } else if (isVolumeGestureEnabled) { - onScrollMainVolume(distanceX, distanceY); - } - - } else /* MainPlayer.PlayerType.POPUP */ { - - // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden -- - final View closingOverlayView = player.getClosingOverlayView(); - final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent); - // Check if an view is in expected state and if not animate it into the correct state - final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE; - if (closingOverlayView.getVisibility() != expectedVisibility) { - animate(closingOverlayView, showClosingOverlayView, 200); - } - } - } - - private void onScrollMainVolume(final float distanceX, final float distanceY) { - // If we just started sliding, change the progress bar to match the system volume - if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { - final float volumePercent = player - .getAudioReactor().getVolume() / (float) maxVolume; - player.getVolumeProgressBar().setProgress( - (int) (volumePercent * player.getMaxGestureLength())); - } - - player.getVolumeProgressBar().incrementProgressBy((int) distanceY); - final float currentProgressPercent = (float) player - .getVolumeProgressBar().getProgress() / player.getMaxGestureLength(); - final int currentVolume = (int) (maxVolume * currentProgressPercent); - player.getAudioReactor().setVolume(currentVolume); - - if (DEBUG) { - Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); - } - - player.getVolumeImageView().setImageDrawable( - AppCompatResources.getDrawable(service, currentProgressPercent <= 0 - ? R.drawable.ic_volume_off - : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute - : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down - : R.drawable.ic_volume_up) - ); - - if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { - animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA); - } - if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - player.getBrightnessRelativeLayout().setVisibility(View.GONE); - } - } - - private void onScrollMainBrightness(final float distanceX, final float distanceY) { - final Activity parent = player.getParentActivity(); - if (parent == null) { - return; - } - - final Window window = parent.getWindow(); - final WindowManager.LayoutParams layoutParams = window.getAttributes(); - final ProgressBar bar = player.getBrightnessProgressBar(); - final float oldBrightness = layoutParams.screenBrightness; - bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); - bar.incrementProgressBy((int) distanceY); - - final float currentProgressPercent = (float) bar.getProgress() / bar.getMax(); - layoutParams.screenBrightness = currentProgressPercent; - window.setAttributes(layoutParams); - - // Save current brightness level - PlayerHelper.setScreenBrightness(parent, currentProgressPercent); - - if (DEBUG) { - Log.d(TAG, "onScroll().brightnessControl, " - + "currentBrightness = " + currentProgressPercent); - } - - player.getBrightnessImageView().setImageDrawable( - AppCompatResources.getDrawable(service, - currentProgressPercent < 0.25 - ? R.drawable.ic_brightness_low - : currentProgressPercent < 0.75 - ? R.drawable.ic_brightness_medium - : R.drawable.ic_brightness_high) - ); - - if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { - animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA); - } - if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - player.getVolumeRelativeLayout().setVisibility(View.GONE); - } - } - - @Override - public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType, - @NonNull final MotionEvent event) { - if (DEBUG) { - Log.d(TAG, "onScrollEnd called with playerType = [" - + player.getPlayerType() + "]"); - } - - if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) { - player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - - if (playerType == MainPlayer.PlayerType.VIDEO) { - if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA, - 200); - } - if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA, - 200); - } - } else /* Popup-Player */ { - if (player.isInsideClosingRadius(event)) { - player.closePopup(); - } else if (!player.isPopupClosing()) { - animate(player.getCloseOverlayButton(), false, 200); - animate(player.getClosingOverlayView(), false, 200); - } - } - } - - @Override - public void onPopupResizingStart() { - if (DEBUG) { - Log.d(TAG, "onPopupResizingStart called"); - } - player.getLoadingPanel().setVisibility(View.GONE); - - player.hideControls(0, 0); - animate(player.getFastSeekOverlay(), false, 0); - animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0); - } - - @Override - public void onPopupResizingEnd() { - if (DEBUG) { - Log.d(TAG, "onPopupResizingEnd called"); - } - } -} - - diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java index 359eab8b2..8c18fd2ad 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -3,6 +3,8 @@ package org.schabi.newpipe.player.event; import com.google.android.exoplayer2.PlaybackException; public interface PlayerServiceEventListener extends PlayerEventListener { + void onViewCreated(); + void onFullscreenStateChanged(boolean fullscreen); void onScreenRotationButtonClicked(); diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java index f774c90a0..8effe2f0e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java @@ -1,11 +1,11 @@ package org.schabi.newpipe.player.event; -import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.Player; public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { void onServiceConnected(Player player, - MainPlayer playerService, + PlayerService playerService, boolean playAfterConnect); void onServiceDisconnected(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt new file mode 100644 index 000000000..555c34f96 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt @@ -0,0 +1,186 @@ +package org.schabi.newpipe.player.gesture + +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import org.schabi.newpipe.databinding.PlayerBinding +import org.schabi.newpipe.player.Player +import org.schabi.newpipe.player.ui.VideoPlayerUi + +/** + * Base gesture handling for [Player] + * + * This class contains the logic for the player gestures like View preparations + * and provides some abstract methods to make it easier separating the logic from the UI. + */ +abstract class BasePlayerGestureListener( + private val playerUi: VideoPlayerUi, +) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { + + protected val player: Player = playerUi.player + protected val binding: PlayerBinding = playerUi.binding + + override fun onTouch(v: View, event: MotionEvent): Boolean { + playerUi.gestureDetector.onTouchEvent(event) + return false + } + + private fun onDoubleTap( + event: MotionEvent, + portion: DisplayPortion + ) { + if (DEBUG) { + Log.d( + TAG, + "onDoubleTap called with playerType = [" + + player.playerType + "], portion = [" + portion + "]" + ) + } + if (playerUi.isSomePopupMenuVisible) { + playerUi.hideControls(0, 0) + } + if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) { + startMultiDoubleTap(event) + } else if (portion === DisplayPortion.MIDDLE) { + player.playPause() + } + } + + protected fun onSingleTap() { + if (playerUi.isControlsVisible) { + playerUi.hideControls(150, 0) + return + } + // -- Controls are not visible -- + + // When player is completed show controls and don't hide them later + if (player.currentState == Player.STATE_COMPLETED) { + playerUi.showControls(0) + } else { + playerUi.showControlsThenHide() + } + } + + open fun onScrollEnd(event: MotionEvent) { + if (DEBUG) { + Log.d( + TAG, + "onScrollEnd called with playerType = [" + + player.playerType + "]" + ) + } + if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) { + playerUi.hideControls( + VideoPlayerUi.DEFAULT_CONTROLS_DURATION, + VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME + ) + } + } + + // /////////////////////////////////////////////////////////////////// + // Simple gestures + // /////////////////////////////////////////////////////////////////// + + override fun onDown(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onDown called with e = [$e]") + + if (isDoubleTapping && isDoubleTapEnabled) { + doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e)) + return true + } + + if (onDownNotDoubleTapping(e)) { + return super.onDown(e) + } + return true + } + + /** + * @return true if `super.onDown(e)` should be called, false otherwise + */ + open fun onDownNotDoubleTapping(e: MotionEvent): Boolean { + return false // do not call super.onDown(e) by default, overridden for popup player + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onDoubleTap called with e = [$e]") + + onDoubleTap(e, getDisplayPortion(e)) + return true + } + + // /////////////////////////////////////////////////////////////////// + // Multi double tapping + // /////////////////////////////////////////////////////////////////// + + private var doubleTapControls: DoubleTapListener? = null + + private val isDoubleTapEnabled: Boolean + get() = doubleTapDelay > 0 + + var isDoubleTapping = false + private set + + fun doubleTapControls(listener: DoubleTapListener) = apply { + doubleTapControls = listener + } + + private var doubleTapDelay = DOUBLE_TAP_DELAY + private val doubleTapHandler: Handler = Handler(Looper.getMainLooper()) + private val doubleTapRunnable = Runnable { + if (DEBUG) + Log.d(TAG, "doubleTapRunnable called") + + isDoubleTapping = false + doubleTapControls?.onDoubleTapFinished() + } + + private fun startMultiDoubleTap(e: MotionEvent) { + if (!isDoubleTapping) { + if (DEBUG) + Log.d(TAG, "startMultiDoubleTap called with e = [$e]") + + keepInDoubleTapMode() + doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e)) + } + } + + fun keepInDoubleTapMode() { + if (DEBUG) + Log.d(TAG, "keepInDoubleTapMode called") + + isDoubleTapping = true + doubleTapHandler.removeCallbacks(doubleTapRunnable) + doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay) + } + + fun endMultiDoubleTap() { + if (DEBUG) + Log.d(TAG, "endMultiDoubleTap called") + + isDoubleTapping = false + doubleTapHandler.removeCallbacks(doubleTapRunnable) + doubleTapControls?.onDoubleTapFinished() + } + + // /////////////////////////////////////////////////////////////////// + // Utils + // /////////////////////////////////////////////////////////////////// + + abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion + + // Currently needed for scrolling since there is no action more the middle portion + abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion + + companion object { + private const val TAG = "BasePlayerGestListener" + private val DEBUG = Player.DEBUG + + private const val DOUBLE_TAP_DELAY = 550L + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java similarity index 87% rename from app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java rename to app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java index a5de56e75..0970dbeb6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.player.event; +package org.schabi.newpipe.player.gesture; import android.content.Context; import android.graphics.Rect; @@ -8,24 +8,25 @@ import android.view.View; import android.widget.FrameLayout; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import org.schabi.newpipe.R; -import java.util.Arrays; import java.util.List; public class CustomBottomSheetBehavior extends BottomSheetBehavior { - public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) { + public CustomBottomSheetBehavior(@NonNull final Context context, + @Nullable final AttributeSet attrs) { super(context, attrs); } Rect globalRect = new Rect(); private boolean skippingInterception = false; - private final List skipInterceptionOfElements = Arrays.asList( + private final List skipInterceptionOfElements = List.of( R.id.detail_content_root_layout, R.id.relatedItemsLayout, R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls, R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); @@ -33,7 +34,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior @Override public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, @NonNull final FrameLayout child, - final MotionEvent event) { + @NonNull final MotionEvent event) { // Drop following when action ends if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP) { @@ -57,7 +58,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior if (getState() == BottomSheetBehavior.STATE_EXPANDED && event.getAction() == MotionEvent.ACTION_DOWN) { // Without overriding scrolling will not work when user touches these elements - for (final Integer element : skipInterceptionOfElements) { + for (final int element : skipInterceptionOfElements) { final View view = child.findViewById(element); if (view != null) { final boolean visible = view.getGlobalVisibleRect(globalRect); diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt similarity index 65% rename from app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt rename to app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt index f15e42897..684f6d326 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.player.event +package org.schabi.newpipe.player.gesture enum class DisplayPortion { LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt new file mode 100644 index 000000000..fc026abd9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt @@ -0,0 +1,7 @@ +package org.schabi.newpipe.player.gesture + +interface DoubleTapListener { + fun onDoubleTapStarted(portion: DisplayPortion) + fun onDoubleTapProgressDown(portion: DisplayPortion) + fun onDoubleTapFinished() +} diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt new file mode 100644 index 000000000..095b3ccdb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt @@ -0,0 +1,234 @@ +package org.schabi.newpipe.player.gesture + +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import android.widget.ProgressBar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.isVisible +import org.schabi.newpipe.MainActivity +import org.schabi.newpipe.R +import org.schabi.newpipe.ktx.AnimationType +import org.schabi.newpipe.ktx.animate +import org.schabi.newpipe.player.Player +import org.schabi.newpipe.player.helper.AudioReactor +import org.schabi.newpipe.player.helper.PlayerHelper +import org.schabi.newpipe.player.ui.MainPlayerUi +import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * GestureListener for the player + * + * While [BasePlayerGestureListener] contains the logic behind the single gestures + * this class focuses on the visual aspect like hiding and showing the controls or changing + * volume/brightness during scrolling for specific events. + */ +class MainPlayerGestureListener( + private val playerUi: MainPlayerUi +) : BasePlayerGestureListener(playerUi), OnTouchListener { + private var isMoving = false + + override fun onTouch(v: View, event: MotionEvent): Boolean { + super.onTouch(v, event) + if (event.action == MotionEvent.ACTION_UP && isMoving) { + isMoving = false + onScrollEnd(event) + } + return when (event.action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { + v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen) + true + } + MotionEvent.ACTION_UP -> { + v.parent?.requestDisallowInterceptTouchEvent(false) + false + } + else -> true + } + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") + + if (isDoubleTapping) + return true + super.onSingleTapConfirmed(e) + + if (player.currentState != Player.STATE_BLOCKED) + onSingleTap() + return true + } + + private fun onScrollVolume(distanceY: Float) { + val bar: ProgressBar = binding.volumeProgressBar + val audioReactor: AudioReactor = player.audioReactor + + // If we just started sliding, change the progress bar to match the system volume + if (!binding.volumeRelativeLayout.isVisible) { + val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat() + bar.progress = (volumePercent * bar.max).toInt() + } + + // Update progress bar + binding.volumeProgressBar.incrementProgressBy(distanceY.toInt()) + + // Update volume + val currentProgressPercent: Float = bar.progress / bar.max.toFloat() + val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt() + audioReactor.volume = currentVolume + if (DEBUG) { + Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume") + } + + // Update player center image + binding.volumeImageView.setImageDrawable( + AppCompatResources.getDrawable( + player.context, + when { + currentProgressPercent <= 0 -> R.drawable.ic_volume_off + currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute + currentProgressPercent < 0.75 -> R.drawable.ic_volume_down + else -> R.drawable.ic_volume_up + } + ) + ) + + // Make sure the correct layout is visible + if (!binding.volumeRelativeLayout.isVisible) { + binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA) + } + binding.brightnessRelativeLayout.isVisible = false + } + + private fun onScrollBrightness(distanceY: Float) { + val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return + val window = parent.window + val layoutParams = window.attributes + val bar: ProgressBar = binding.brightnessProgressBar + + // Update progress bar + val oldBrightness = layoutParams.screenBrightness + bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt() + bar.incrementProgressBy(distanceY.toInt()) + + // Update brightness + val currentProgressPercent = bar.progress.toFloat() / bar.max + layoutParams.screenBrightness = currentProgressPercent + window.attributes = layoutParams + + // Save current brightness level + PlayerHelper.setScreenBrightness(parent, currentProgressPercent) + if (DEBUG) { + Log.d( + TAG, + "onScroll().brightnessControl, " + + "currentBrightness = " + currentProgressPercent + ) + } + + // Update player center image + binding.brightnessImageView.setImageDrawable( + AppCompatResources.getDrawable( + player.context, + when { + currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low + currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium + else -> R.drawable.ic_brightness_high + } + ) + ) + + // Make sure the correct layout is visible + if (!binding.brightnessRelativeLayout.isVisible) { + binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA) + } + binding.volumeRelativeLayout.isVisible = false + } + + override fun onScrollEnd(event: MotionEvent) { + super.onScrollEnd(event) + if (binding.volumeRelativeLayout.isVisible) { + binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200) + } + if (binding.brightnessRelativeLayout.isVisible) { + binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200) + } + } + + override fun onScroll( + initialEvent: MotionEvent, + movingEvent: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + + if (!playerUi.isFullscreen) { + return false + } + + // Calculate heights of status and navigation bars + val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height") + val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height") + + // Do not handle this event if initially it started from status or navigation bars + val isTouchingStatusBar = initialEvent.y < statusBarHeight + val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight) + if (isTouchingStatusBar || isTouchingNavigationBar) { + return false + } + + val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD + if ( + !isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) || + player.currentState == Player.STATE_COMPLETED + ) { + return false + } + + isMoving = true + + // -- Brightness and Volume control -- + val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context) + val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context) + if (isBrightnessGestureEnabled && isVolumeGestureEnabled) { + if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) { + onScrollBrightness(distanceY) + } else /* DisplayPortion.RIGHT_HALF */ { + onScrollVolume(distanceY) + } + } else if (isBrightnessGestureEnabled) { + onScrollBrightness(distanceY) + } else if (isVolumeGestureEnabled) { + onScrollVolume(distanceY) + } + + return true + } + + override fun getDisplayPortion(e: MotionEvent): DisplayPortion { + return when { + e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT + e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT + else -> DisplayPortion.MIDDLE + } + } + + override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { + return when { + e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF + else -> DisplayPortion.RIGHT_HALF + } + } + + companion object { + private val TAG = MainPlayerGestureListener::class.java.simpleName + private val DEBUG = MainActivity.DEBUG + private const val MOVEMENT_THRESHOLD = 40 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt new file mode 100644 index 000000000..666ea6a46 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt @@ -0,0 +1,283 @@ +package org.schabi.newpipe.player.gesture + +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import androidx.core.math.MathUtils +import org.schabi.newpipe.MainActivity +import org.schabi.newpipe.ktx.AnimationType +import org.schabi.newpipe.ktx.animate +import org.schabi.newpipe.player.ui.PopupPlayerUi +import kotlin.math.abs +import kotlin.math.hypot +import kotlin.math.max +import kotlin.math.min + +class PopupPlayerGestureListener( + private val playerUi: PopupPlayerUi, +) : BasePlayerGestureListener(playerUi) { + + private var isMoving = false + + private var initialPopupX: Int = -1 + private var initialPopupY: Int = -1 + private var isResizing = false + + // initial coordinates and distance between fingers + private var initPointerDistance = -1.0 + private var initFirstPointerX = -1f + private var initFirstPointerY = -1f + private var initSecPointerX = -1f + private var initSecPointerY = -1f + + override fun onTouch(v: View, event: MotionEvent): Boolean { + super.onTouch(v, event) + if (event.pointerCount == 2 && !isMoving && !isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") + } + onPopupResizingStart() + + // record coordinates of fingers + initFirstPointerX = event.getX(0) + initFirstPointerY = event.getY(0) + initSecPointerX = event.getX(1) + initSecPointerY = event.getY(1) + // record distance between fingers + initPointerDistance = hypot( + initFirstPointerX - initSecPointerX.toDouble(), + initFirstPointerY - initSecPointerY.toDouble() + ) + + isResizing = true + } + if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) { + if (DEBUG) { + Log.d( + TAG, + "onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" + + "[${event.rawX}, ${event.rawY}]" + ) + } + return handleMultiDrag(event) + } + if (event.action == MotionEvent.ACTION_UP) { + if (DEBUG) { + Log.d( + TAG, + "onTouch() ACTION_UP > v = [$v], e1.getRaw =" + + " [${event.rawX}, ${event.rawY}]" + ) + } + if (isMoving) { + isMoving = false + onScrollEnd(event) + } + if (isResizing) { + isResizing = false + + initPointerDistance = (-1).toDouble() + initFirstPointerX = (-1).toFloat() + initFirstPointerY = (-1).toFloat() + initSecPointerX = (-1).toFloat() + initSecPointerY = (-1).toFloat() + + onPopupResizingEnd() + player.changeState(player.currentState) + } + if (!playerUi.isPopupClosing) { + playerUi.savePopupPositionAndSizeToPrefs() + } + } + + v.performClick() + return true + } + + override fun onScrollEnd(event: MotionEvent) { + super.onScrollEnd(event) + if (playerUi.isInsideClosingRadius(event)) { + playerUi.closePopup() + } else if (!playerUi.isPopupClosing) { + playerUi.closeOverlayBinding.closeButton.animate(false, 200) + binding.closingOverlay.animate(false, 200) + } + } + + private fun handleMultiDrag(event: MotionEvent): Boolean { + if (initPointerDistance == -1.0 || event.pointerCount != 2) { + return false + } + + // get the movements of the fingers + val firstPointerMove = hypot( + event.getX(0) - initFirstPointerX.toDouble(), + event.getY(0) - initFirstPointerY.toDouble() + ) + val secPointerMove = hypot( + event.getX(1) - initSecPointerX.toDouble(), + event.getY(1) - initSecPointerY.toDouble() + ) + + // minimum threshold beyond which pinch gesture will work + val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop + if (max(firstPointerMove, secPointerMove) <= minimumMove) { + return false + } + + // calculate current distance between the pointers + val currentPointerDistance = hypot( + event.getX(0) - event.getX(1).toDouble(), + event.getY(0) - event.getY(1).toDouble() + ) + + val popupWidth = playerUi.popupLayoutParams.width.toDouble() + // change co-ordinates of popup so the center stays at the same position + val newWidth = popupWidth * currentPointerDistance / initPointerDistance + initPointerDistance = currentPointerDistance + playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt() + + playerUi.checkPopupPositionBounds() + playerUi.updateScreenSize() + playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt()) + return true + } + + private fun onPopupResizingStart() { + if (DEBUG) { + Log.d(TAG, "onPopupResizingStart called") + } + binding.loadingPanel.visibility = View.GONE + playerUi.hideControls(0, 0) + binding.fastSeekOverlay.animate(false, 0) + binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0) + } + + private fun onPopupResizingEnd() { + if (DEBUG) { + Log.d(TAG, "onPopupResizingEnd called") + } + } + + override fun onLongPress(e: MotionEvent?) { + playerUi.updateScreenSize() + playerUi.checkPopupPositionBounds() + playerUi.changePopupSize(playerUi.screenWidth) + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + return if (player.popupPlayerSelected()) { + val absVelocityX = abs(velocityX) + val absVelocityY = abs(velocityY) + if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) { + if (absVelocityX > TOSS_FLING_VELOCITY) { + playerUi.popupLayoutParams.x = velocityX.toInt() + } + if (absVelocityY > TOSS_FLING_VELOCITY) { + playerUi.popupLayoutParams.y = velocityY.toInt() + } + playerUi.checkPopupPositionBounds() + playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams) + return true + } + return false + } else { + true + } + } + + override fun onDownNotDoubleTapping(e: MotionEvent): Boolean { + // Fix popup position when the user touch it, it may have the wrong one + // because the soft input is visible (the draggable area is currently resized). + playerUi.updateScreenSize() + playerUi.checkPopupPositionBounds() + playerUi.popupLayoutParams.let { + initialPopupX = it.x + initialPopupY = it.y + } + return true // we want `super.onDown(e)` to be called + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (DEBUG) + Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") + + if (isDoubleTapping) + return true + if (player.exoPlayerIsNull()) + return false + + onSingleTap() + return true + } + + override fun onScroll( + initialEvent: MotionEvent, + movingEvent: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + + if (isResizing) { + return super.onScroll(initialEvent, movingEvent, distanceX, distanceY) + } + + if (!isMoving) { + playerUi.closeOverlayBinding.closeButton.animate(true, 200) + } + + isMoving = true + + val diffX = (movingEvent.rawX - initialEvent.rawX) + val posX = MathUtils.clamp( + initialPopupX + diffX, + 0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat() + ) + val diffY = (movingEvent.rawY - initialEvent.rawY) + val posY = MathUtils.clamp( + initialPopupY + diffY, + 0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat() + ) + + playerUi.popupLayoutParams.x = posX.toInt() + playerUi.popupLayoutParams.y = posY.toInt() + + // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden -- + val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent) + // Check if an view is in expected state and if not animate it into the correct state + val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE + if (binding.closingOverlay.visibility != expectedVisibility) { + binding.closingOverlay.animate(showClosingOverlayView, 200) + } + + playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams) + return true + } + + override fun getDisplayPortion(e: MotionEvent): DisplayPortion { + return when { + e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT + e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT + else -> DisplayPortion.MIDDLE + } + } + + override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { + return when { + e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF + else -> DisplayPortion.RIGHT_HALF + } + } + + companion object { + private val TAG = PopupPlayerGestureListener::class.java.simpleName + private val DEBUG = MainActivity.DEBUG + private const val TOSS_FLING_VELOCITY = 2500 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index d189616d1..41fcc823a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -39,8 +39,8 @@ final class CacheFactory implements DataSource.Factory { .createDataSource(); final FileDataSource fileSource = new FileDataSource(); - final CacheDataSink dataSink - = new CacheDataSink(cache, PlayerHelper.getPreferredFileSize()); + final CacheDataSink dataSink = + new CacheDataSink(cache, PlayerHelper.getPreferredFileSize()); return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java deleted file mode 100644 index a8735dc08..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ /dev/null @@ -1,226 +0,0 @@ -package org.schabi.newpipe.player.helper; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import android.util.Log; -import android.view.KeyEvent; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.media.session.MediaButtonReceiver; - -import com.google.android.exoplayer2.ForwardingPlayer; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.player.mediasession.MediaSessionCallback; -import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; - -import java.util.Optional; - -public class MediaSessionManager { - private static final String TAG = MediaSessionManager.class.getSimpleName(); - public static final boolean DEBUG = MainActivity.DEBUG; - - @NonNull - private final MediaSessionCompat mediaSession; - @NonNull - private final MediaSessionConnector sessionConnector; - - private int lastTitleHashCode; - private int lastArtistHashCode; - private long lastDuration; - private int lastAlbumArtHashCode; - - public MediaSessionManager(@NonNull final Context context, - @NonNull final Player player, - @NonNull final MediaSessionCallback callback) { - mediaSession = new MediaSessionCompat(context, TAG); - mediaSession.setActive(true); - - mediaSession.setPlaybackState(new PlaybackStateCompat.Builder() - .setState(PlaybackStateCompat.STATE_NONE, -1, 1) - .setActions(PlaybackStateCompat.ACTION_SEEK_TO - | PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_SET_REPEAT_MODE - | PlaybackStateCompat.ACTION_STOP) - .build()); - - sessionConnector = new MediaSessionConnector(mediaSession); - sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); - sessionConnector.setPlayer(new ForwardingPlayer(player) { - @Override - public void play() { - callback.play(); - } - - @Override - public void pause() { - callback.pause(); - } - }); - } - - @Nullable - @SuppressWarnings("UnusedReturnValue") - public KeyEvent handleMediaButtonIntent(final Intent intent) { - return MediaButtonReceiver.handleIntent(mediaSession, intent); - } - - public MediaSessionCompat.Token getSessionToken() { - return mediaSession.getSessionToken(); - } - - /** - * sets the Metadata - if required. - * - * @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE} - * @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST} - * @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART} - * @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION} - * - should be a negative value for unknown durations, e.g. for livestreams - */ - public void setMetadata(@NonNull final String title, - @NonNull final String artist, - @NonNull final Optional optAlbumArt, - final long duration - ) { - if (DEBUG) { - Log.d(TAG, "setMetadata called:" - + " t: " + title - + " a: " + artist - + " thumb: " + ( - optAlbumArt.isPresent() - ? optAlbumArt.get().hashCode() - : "") - + " d: " + duration); - } - - if (!mediaSession.isActive()) { - if (DEBUG) { - Log.d(TAG, "setMetadata: mediaSession not active - exiting"); - } - return; - } - - if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) { - if (DEBUG) { - Log.d(TAG, "setMetadata: No update required - exiting"); - } - return; - } - - if (DEBUG) { - Log.d(TAG, "setMetadata: N_Metadata update:" - + " t: " + title - + " a: " + artist - + " thumb: " + ( - optAlbumArt.isPresent() - ? optAlbumArt.get().hashCode() - : "") - + " d: " + duration); - } - - final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration); - - if (optAlbumArt.isPresent()) { - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get()); - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get()); - } - - mediaSession.setMetadata(builder.build()); - - lastTitleHashCode = title.hashCode(); - lastArtistHashCode = artist.hashCode(); - lastDuration = duration; - optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode()); - } - - private boolean checkIfMetadataShouldBeSet( - @NonNull final String title, - @NonNull final String artist, - @NonNull final Optional optAlbumArt, - final long duration - ) { - // Check if the values have changed since the last time - if (title.hashCode() != lastTitleHashCode - || artist.hashCode() != lastArtistHashCode - || duration != lastDuration - || (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode) - ) { - if (DEBUG) { - Log.d(TAG, - "checkIfMetadataShouldBeSet: true - reason: changed values since last"); - } - return true; - } - - // Check if the currently set metadata is valid - if (getMetadataTitle() == null - || getMetadataArtist() == null - // Note that the duration can be <= 0 for live streams - ) { - if (DEBUG) { - if (getMetadataTitle() == null) { - Log.d(TAG, - "N_getMetadataTitle: title == null"); - } else if (getMetadataArtist() == null) { - Log.d(TAG, - "N_getMetadataArtist: artist == null"); - } - } - return true; - } - - // If we got an album art check if the current set AlbumArt is null - if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) { - if (DEBUG) { - Log.d(TAG, "N_getMetadataAlbumArt: thumb == null"); - } - return true; - } - - // Default - no update required - return false; - } - - - @Nullable - private Bitmap getMetadataAlbumArt() { - return mediaSession.getController().getMetadata() - .getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART); - } - - @Nullable - private String getMetadataTitle() { - return mediaSession.getController().getMetadata() - .getString(MediaMetadataCompat.METADATA_KEY_TITLE); - } - - @Nullable - private String getMetadataArtist() { - return mediaSession.getController().getMetadata() - .getString(MediaMetadataCompat.METADATA_KEY_ARTIST); - } - - /** - * Should be called on player destruction to prevent leakage. - */ - public void dispose() { - sessionConnector.setPlayer(null); - sessionConnector.setQueueNavigator(null); - mediaSession.setActive(false); - mediaSession.release(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 19a5a645b..796208a04 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -11,7 +11,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.widget.CheckBox; import android.widget.SeekBar; @@ -21,16 +20,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; +import androidx.core.math.MathUtils; import androidx.fragment.app.DialogFragment; import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding; -import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SliderStrategy; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -149,7 +148,7 @@ public class PlaybackParameterDialog extends DialogFragment { assureCorrectAppLanguage(getContext()); Icepick.restoreInstanceState(this, savedInstanceState); - binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext())); + binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater()); initUI(); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) @@ -207,7 +206,7 @@ public class PlaybackParameterDialog extends DialogFragment { ? View.VISIBLE : View.GONE); animateRotation(binding.pitchToogleControlModes, - Player.DEFAULT_CONTROLS_DURATION, + VideoPlayerUi.DEFAULT_CONTROLS_DURATION, isCurrentlyVisible ? 180 : 0); }); @@ -334,10 +333,8 @@ public class PlaybackParameterDialog extends DialogFragment { } private Map getPitchControlModeComponentMappings() { - final Map mappings = new HashMap<>(); - mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent); - mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone); - return mappings; + return Map.of(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent, + PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone); } private void changePitchControlMode(final boolean semitones) { @@ -407,13 +404,11 @@ public class PlaybackParameterDialog extends DialogFragment { } private Map getStepSizeComponentMappings() { - final Map mappings = new HashMap<>(); - mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent); - mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent); - mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent); - mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent); - mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent); - return mappings; + return Map.of(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent, + STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent, + STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent, + STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent, + STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent); } private void setStepSizeToUI(final double newStepSize) { @@ -532,7 +527,7 @@ public class PlaybackParameterDialog extends DialogFragment { } private void setAndUpdateTempo(final double newTempo) { - this.tempo = calcValidTempo(newTempo); + this.tempo = MathUtils.clamp(newTempo, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED); binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo)); setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo); @@ -551,13 +546,8 @@ public class PlaybackParameterDialog extends DialogFragment { pitchPercent); } - private double calcValidTempo(final double newTempo) { - return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo)); - } - private double calcValidPitch(final double newPitch) { - final double calcPitch = - Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch)); + final double calcPitch = MathUtils.clamp(newPitch, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED); if (!isCurrentPitchControlModeSemitone()) { return calcPitch; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 88f25e194..0530d56e9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -208,8 +208,8 @@ public class PlayerDataSource { Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir"); } - final LeastRecentlyUsedCacheEvictor evictor - = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize()); + final LeastRecentlyUsedCacheEvictor evictor = + new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize()); cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context)); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 2131861bf..abde7c3d1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -3,8 +3,6 @@ package org.schabi.newpipe.player.helper; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS; -import static org.schabi.newpipe.player.Player.PLAYER_TYPE; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; @@ -15,14 +13,8 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.PixelFormat; -import android.os.Build; import android.provider.Settings; -import android.view.Gravity; -import android.view.ViewGroup; -import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -49,7 +41,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.utils.Utils; -import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -71,25 +62,11 @@ import java.util.concurrent.TimeUnit; public final class PlayerHelper { private static final StringBuilder STRING_BUILDER = new StringBuilder(); - private static final Formatter STRING_FORMATTER - = new Formatter(STRING_BUILDER, Locale.getDefault()); + private static final Formatter STRING_FORMATTER = + new Formatter(STRING_BUILDER, Locale.getDefault()); private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x"); private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%"); - /** - * Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using - * NewPipe's popup player. - * - *

- * This value is hardcoded instead of being get dynamically with the method linked of the - * constant documentation below, because it is not static and popup player layout parameters - * are generated with static methods. - *

- * - * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE - */ - private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f; - @Retention(SOURCE) @IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI, AUTOPLAY_TYPE_NEVER}) @@ -339,10 +316,6 @@ public final class PlayerHelper { return true; } - public static int getTossFlingVelocity() { - return 2500; - } - @NonNull public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { final CaptioningManager captioningManager = ContextCompat.getSystemService(context, @@ -452,12 +425,6 @@ public final class PlayerHelper { // Utils used by player //////////////////////////////////////////////////////////////////////////// - public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) { - // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra - return MainPlayer.PlayerType.values()[ - intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())]; - } - public static boolean isPlaybackResumeEnabled(final Player player) { return player.getPrefs().getBoolean( player.getContext().getString(R.string.enable_watch_history_key), true) @@ -528,90 +495,10 @@ public final class PlayerHelper { .apply(); } - /** - * @param player {@code screenWidth} and {@code screenHeight} must have been initialized - * @return the popup starting layout params - */ - @SuppressLint("RtlHardcoded") - public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs( - final Player player) { - final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean( - player.getContext().getString(R.string.popup_remember_size_pos_key), true); - final float defaultSize = - player.getContext().getResources().getDimension(R.dimen.popup_default_width); - final float popupWidth = popupRememberSizeAndPos - ? player.getPrefs().getFloat(player.getContext().getString( - R.string.popup_saved_width_key), defaultSize) - : defaultSize; - final float popupHeight = getMinimumVideoHeight(popupWidth); - - final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams( - (int) popupWidth, (int) popupHeight, - popupLayoutParamType(), - IDLE_WINDOW_FLAGS, - PixelFormat.TRANSLUCENT); - popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - - final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f); - final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); - popupLayoutParams.x = popupRememberSizeAndPos - ? player.getPrefs().getInt(player.getContext().getString( - R.string.popup_saved_x_key), centerX) : centerX; - popupLayoutParams.y = popupRememberSizeAndPos - ? player.getPrefs().getInt(player.getContext().getString( - R.string.popup_saved_y_key), centerY) : centerY; - - return popupLayoutParams; - } - - public static void savePopupPositionAndSizeToPrefs(final Player player) { - if (player.getPopupLayoutParams() != null) { - player.getPrefs().edit() - .putFloat(player.getContext().getString(R.string.popup_saved_width_key), - player.getPopupLayoutParams().width) - .putInt(player.getContext().getString(R.string.popup_saved_x_key), - player.getPopupLayoutParams().x) - .putInt(player.getContext().getString(R.string.popup_saved_y_key), - player.getPopupLayoutParams().y) - .apply(); - } - } - public static float getMinimumVideoHeight(final float width) { return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have } - @SuppressLint("RtlHardcoded") - public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() { - final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - - final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - popupLayoutParamType(), - flags, - PixelFormat.TRANSLUCENT); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // Setting maximum opacity allowed for touch events to other apps for Android 12 and - // higher to prevent non interaction when using other apps with the popup player - closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER; - } - - closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - closeOverlayLayoutParams.softInputMode = - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - return closeOverlayLayoutParams; - } - - public static int popupLayoutParamType() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - } - public static int retrieveSeekDurationFromPreferences(final Player player) { return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString( player.getContext().getString(R.string.seek_duration_key), diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 4c09ed3c1..5eaecd48d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -16,8 +16,9 @@ import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.App; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.Player; +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; @@ -42,17 +43,17 @@ public final class PlayerHolder { private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); private boolean bound; - @Nullable private MainPlayer playerService; + @Nullable private PlayerService playerService; @Nullable private Player player; /** - * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, - * otherwise `null` if no service running. + * Returns the current {@link PlayerType} of the {@link PlayerService} service, + * otherwise `null` if no service is running. * * @return Current PlayerType */ @Nullable - public MainPlayer.PlayerType getType() { + public PlayerType getType() { if (player == null) { return null; } @@ -122,7 +123,7 @@ 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, MainPlayer.class)); + ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class)); serviceConnection.doPlayAfterConnect(playAfterConnect); bind(context); } @@ -130,7 +131,7 @@ public final class PlayerHolder { public void stopService() { final Context context = getCommonContext(); unbind(context); - context.stopService(new Intent(context, MainPlayer.class)); + context.stopService(new Intent(context, PlayerService.class)); } class PlayerServiceConnection implements ServiceConnection { @@ -156,7 +157,7 @@ public final class PlayerHolder { if (DEBUG) { Log.d(TAG, "Player service is connected"); } - final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; + final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service; playerService = localBinder.getService(); player = localBinder.getPlayer(); @@ -172,7 +173,7 @@ public final class PlayerHolder { Log.d(TAG, "bind() called"); } - final Intent serviceIntent = new Intent(context, MainPlayer.class); + final Intent serviceIntent = new Intent(context, PlayerService.class); bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); if (!bound) { @@ -211,6 +212,13 @@ public final class PlayerHolder { private final PlayerServiceEventListener internalListener = new PlayerServiceEventListener() { + @Override + public void onViewCreated() { + if (listener != null) { + listener.onViewCreated(); + } + } + @Override public void onFullscreenStateChanged(final boolean fullscreen) { if (listener != null) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java index f3a71d7cd..f1ba90f8e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.player.helper; +import androidx.core.math.MathUtils; + /** * Converts between percent and 12-tone equal temperament semitones. *
@@ -33,6 +35,6 @@ public final class PlayerSemitoneHelper { } private static int ensureSemitonesInRange(final int semitones) { - return Math.max(-SEMITONE_COUNT, Math.min(SEMITONE_COUNT, semitones)); + return MathUtils.clamp(semitones, -SEMITONE_COUNT, SEMITONE_COUNT); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt deleted file mode 100644 index 52eff5a1c..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.schabi.newpipe.player.listeners.view - -import android.util.Log -import android.view.View -import androidx.appcompat.widget.PopupMenu -import org.schabi.newpipe.MainActivity -import org.schabi.newpipe.player.Player -import org.schabi.newpipe.player.helper.PlaybackParameterDialog - -/** - * Click listener for the playbackSpeed textview of the player - */ -class PlaybackSpeedClickListener( - private val player: Player, - private val playbackSpeedPopupMenu: PopupMenu -) : View.OnClickListener { - - companion object { - private const val TAG: String = "PlaybSpeedClickListener" - } - - override fun onClick(v: View) { - if (MainActivity.DEBUG) { - Log.d(TAG, "onPlaybackSpeedClicked() called") - } - - if (player.videoPlayerSelected()) { - PlaybackParameterDialog.newInstance( - player.playbackSpeed.toDouble(), - player.playbackPitch.toDouble(), - player.playbackSkipSilence - ) { speed: Float, pitch: Float, skipSilence: Boolean -> - player.setPlaybackParameters( - speed, - pitch, - skipSilence - ) - } - .show(player.parentActivity!!.supportFragmentManager, null) - } else { - playbackSpeedPopupMenu.show() - player.isSomePopupMenuVisible = true - } - - player.manageControlsAfterOnClick(v) - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt deleted file mode 100644 index 43e8288e6..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.schabi.newpipe.player.listeners.view - -import android.annotation.SuppressLint -import android.util.Log -import android.view.View -import androidx.appcompat.widget.PopupMenu -import org.schabi.newpipe.MainActivity -import org.schabi.newpipe.extractor.MediaFormat -import org.schabi.newpipe.player.Player - -/** - * Click listener for the qualityTextView of the player - */ -class QualityClickListener( - private val player: Player, - private val qualityPopupMenu: PopupMenu -) : View.OnClickListener { - - companion object { - private const val TAG: String = "QualityClickListener" - } - - @SuppressLint("SetTextI18n") // we don't need I18N because of a " " - override fun onClick(v: View) { - if (MainActivity.DEBUG) { - Log.d(TAG, "onQualitySelectorClicked() called") - } - - qualityPopupMenu.show() - player.isSomePopupMenuVisible = true - - val videoStream = player.selectedVideoStream - if (videoStream != null) { - player.binding.qualityTextView.text = - MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution() - } - - player.saveWasPlaying() - player.manageControlsAfterOnClick(v) - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java index f84b0383a..d23dd4574 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.player.mediaitem; import android.net.Uri; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.RequestMetadata; import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.Player; @@ -76,7 +77,6 @@ public interface MediaItemTag { @NonNull default MediaItem asMediaItem() { final MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setMediaUri(Uri.parse(getStreamUrl())) .setArtworkUri(Uri.parse(getThumbnailUrl())) .setArtist(getUploaderName()) .setDescription(getTitle()) @@ -84,10 +84,15 @@ public interface MediaItemTag { .setTitle(getTitle()) .build(); + final RequestMetadata requestMetaData = new RequestMetadata.Builder() + .setMediaUri(Uri.parse(getStreamUrl())) + .build(); + return MediaItem.fromUri(getStreamUrl()) .buildUpon() .setMediaId(makeMediaId()) .setMediaMetadata(mediaMetadata) + .setRequestMetadata(requestMetaData) .setTag(this) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java deleted file mode 100644 index c4b02d985..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.schabi.newpipe.player.mediasession; - -import android.support.v4.media.MediaDescriptionCompat; - -public interface MediaSessionCallback { - void playPrevious(); - - void playNext(); - - void playItemAtIndex(int index); - - int getCurrentPlayingIndex(); - - int getQueueSize(); - - MediaDescriptionCompat getQueueMetadata(int index); - - void play(); - - void pause(); -} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java new file mode 100644 index 000000000..e9541ab06 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java @@ -0,0 +1,134 @@ +package org.schabi.newpipe.player.mediasession; + +import static org.schabi.newpipe.MainActivity.DEBUG; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.session.MediaButtonReceiver; + +import com.google.android.exoplayer2.ForwardingPlayer; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.ui.PlayerUi; +import org.schabi.newpipe.player.ui.VideoPlayerUi; +import org.schabi.newpipe.util.StreamTypeUtil; + +import java.util.Optional; + +public class MediaSessionPlayerUi extends PlayerUi { + private static final String TAG = "MediaSessUi"; + + private MediaSessionCompat mediaSession; + private MediaSessionConnector sessionConnector; + + public MediaSessionPlayerUi(@NonNull final Player player) { + super(player); + } + + @Override + public void initPlayer() { + 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()); + + sessionConnector.setMetadataDeduplicationEnabled(true); + sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata()); + } + + @Override + public void destroyPlayer() { + super.destroyPlayer(); + if (sessionConnector != null) { + sessionConnector.setPlayer(null); + sessionConnector.setQueueNavigator(null); + sessionConnector = null; + } + if (mediaSession != null) { + mediaSession.setActive(false); + mediaSession.release(); + mediaSession = null; + } + } + + @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(); + } + } + + + public void handleMediaButtonIntent(final Intent intent) { + MediaButtonReceiver.handleIntent(mediaSession, intent); + } + + public Optional getSessionToken() { + return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken); + } + + + private ForwardingPlayer getForwardingPlayer() { + // ForwardingPlayer means that all media session actions called on this player are + // forwarded directly to the connected exoplayer, except for the overridden methods. So + // override play and pause since our player adds more functionality to them over exoplayer. + return new ForwardingPlayer(player.getExoPlayer()) { + @Override + public void play() { + player.play(); + // hide the player controls even if the play command came from the media session + player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); + } + + @Override + public void pause() { + player.pause(); + } + }; + } + + private MediaMetadataCompat buildMediaMetadata() { + if (DEBUG) { + Log.d(TAG, "buildMediaMetadata called"); + } + + // set title and artist + final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, player.getVideoTitle()) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, player.getUploaderName()); + + // set duration (-1 for livestreams or if unknown, see the METADATA_KEY_DURATION docs) + final long duration = player.getCurrentStreamInfo() + .filter(info -> !StreamTypeUtil.isLiveStream(info.getStreamType())) + .map(info -> info.getDuration() * 1000L) + .orElse(-1L); + builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration); + + // set album art, unless the user asked not to, or there is no thumbnail available + final boolean showThumbnail = player.getPrefs().getBoolean( + context.getString(R.string.show_thumbnail_key), true); + Optional.ofNullable(player.getThumbnail()) + .filter(bitmap -> showThumbnail) + .ifPresent(bitmap -> { + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap); + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap); + }); + + return builder.build(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index 92cd425c5..2e54b1129 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -1,106 +1,152 @@ package org.schabi.newpipe.player.mediasession; -import android.os.Bundle; -import android.os.ResultReceiver; -import android.support.v4.media.session.MediaSessionCompat; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import com.google.android.exoplayer2.util.Util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; +import android.net.Uri; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import com.google.android.exoplayer2.util.Util; + +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { - public static final int DEFAULT_MAX_QUEUE_SIZE = 10; + private static final int MAX_QUEUE_SIZE = 10; private final MediaSessionCompat mediaSession; - private final MediaSessionCallback callback; - private final int maxQueueSize; + private final Player player; private long activeQueueItemId; public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, - @NonNull final MediaSessionCallback callback) { + @NonNull final Player player) { this.mediaSession = mediaSession; - this.callback = callback; - this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + this.player = player; this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; } @Override - public long getSupportedQueueNavigatorActions(@Nullable final Player player) { + public long getSupportedQueueNavigatorActions( + @Nullable final com.google.android.exoplayer2.Player exoPlayer) { return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; } @Override - public void onTimelineChanged(@NonNull final Player player) { + public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) { publishFloatingQueueWindow(); } @Override - public void onCurrentMediaItemIndexChanged(@NonNull final Player player) { + public void onCurrentMediaItemIndexChanged( + @NonNull final com.google.android.exoplayer2.Player exoPlayer) { if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID - || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { + || exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) { publishFloatingQueueWindow(); - } else if (!player.getCurrentTimeline().isEmpty()) { - activeQueueItemId = player.getCurrentMediaItemIndex(); + } else if (!exoPlayer.getCurrentTimeline().isEmpty()) { + activeQueueItemId = exoPlayer.getCurrentMediaItemIndex(); } } @Override - public long getActiveQueueItemId(@Nullable final Player player) { - return callback.getCurrentPlayingIndex(); + public long getActiveQueueItemId( + @Nullable final com.google.android.exoplayer2.Player exoPlayer) { + return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1); } @Override - public void onSkipToPrevious(@NonNull final Player player) { - callback.playPrevious(); + public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) { + player.playPrevious(); } @Override - public void onSkipToQueueItem(@NonNull final Player player, final long id) { - callback.playItemAtIndex((int) id); + public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer, + final long id) { + if (player.getPlayQueue() != null) { + player.selectQueueItem(player.getPlayQueue().getItem((int) id)); + } } @Override - public void onSkipToNext(@NonNull final Player player) { - callback.playNext(); + public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) { + player.playNext(); } private void publishFloatingQueueWindow() { - if (callback.getQueueSize() == 0) { + final int windowCount = Optional.ofNullable(player.getPlayQueue()) + .map(PlayQueue::size) + .orElse(0); + if (windowCount == 0) { mediaSession.setQueue(Collections.emptyList()); activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; return; } // Yes this is almost a copypasta, got a problem with that? =\ - final int windowCount = callback.getQueueSize(); - final int currentWindowIndex = callback.getCurrentPlayingIndex(); - final int queueSize = Math.min(maxQueueSize, windowCount); + final int currentWindowIndex = player.getPlayQueue().getIndex(); + final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount); final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, windowCount - queueSize); final List queue = new ArrayList<>(); for (int i = startIndex; i < startIndex + queueSize; i++) { - queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); + queue.add(new MediaSessionCompat.QueueItem(getQueueMetadata(i), i)); } mediaSession.setQueue(queue); activeQueueItemId = currentWindowIndex; } + public MediaDescriptionCompat getQueueMetadata(final int index) { + if (player.getPlayQueue() == null) { + return null; + } + final PlayQueueItem item = player.getPlayQueue().getItem(index); + if (item == null) { + return null; + } + + final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder() + .setMediaId(String.valueOf(index)) + .setTitle(item.getTitle()) + .setSubtitle(item.getUploader()); + + // set additional metadata for A2DP/AVRCP (Audio/Video Bluetooth profiles) + final Bundle additionalMetadata = new Bundle(); + additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle()); + additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader()); + additionalMetadata + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000); + additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1L); + additionalMetadata + .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size()); + descBuilder.setExtras(additionalMetadata); + + final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); + if (thumbnailUri != null) { + descBuilder.setIconUri(thumbnailUri); + } + + return descBuilder.build(); + } + @Override - public boolean onCommand(@NonNull final Player player, + public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer, @NonNull final String command, @Nullable final Bundle extras, @Nullable final ResultReceiver cb) { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 8aad356d0..b9ca90d89 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -16,7 +16,7 @@ import org.schabi.newpipe.player.mediaitem.ExceptionTag; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; -import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; @@ -56,9 +56,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo this.playQueueItem = playQueueItem; this.error = error; this.retryTimestamp = retryTimestamp; - this.mediaItem = ExceptionTag - .of(playQueueItem, Collections.singletonList(error)) - .withExtras(this) + this.mediaItem = ExceptionTag.of(playQueueItem, List.of(error)).withExtras(this) .asMediaItem(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java index 6c9858d1b..89bf0b22a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.player; +package org.schabi.newpipe.player.notification; import android.content.Context; import android.content.SharedPreferences; @@ -7,20 +7,48 @@ import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.util.Localization; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; public final class NotificationConstants { - private NotificationConstants() { } + private NotificationConstants() { + } + + + + /*////////////////////////////////////////////////////////////////////////// + // Intent actions + //////////////////////////////////////////////////////////////////////////*/ + + private static final String BASE_ACTION = + App.PACKAGE_NAME + ".player.MainPlayer."; + public static final String ACTION_CLOSE = + BASE_ACTION + "CLOSE"; + public static final String ACTION_PLAY_PAUSE = + BASE_ACTION + ".player.MainPlayer.PLAY_PAUSE"; + public static final String ACTION_REPEAT = + BASE_ACTION + ".player.MainPlayer.REPEAT"; + public static final String ACTION_PLAY_NEXT = + BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_NEXT"; + public static final String ACTION_PLAY_PREVIOUS = + BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_PREVIOUS"; + public static final String ACTION_FAST_REWIND = + BASE_ACTION + ".player.MainPlayer.ACTION_FAST_REWIND"; + public static final String ACTION_FAST_FORWARD = + BASE_ACTION + ".player.MainPlayer.ACTION_FAST_FORWARD"; + public static final String ACTION_SHUFFLE = + BASE_ACTION + ".player.MainPlayer.ACTION_SHUFFLE"; + public static final String ACTION_RECREATE_NOTIFICATION = + BASE_ACTION + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; public static final int NOTHING = 0; @@ -86,7 +114,7 @@ public final class NotificationConstants { }; - public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2}; + public static final List SLOT_COMPACT_DEFAULTS = List.of(0, 1, 2); public static final int[] SLOT_COMPACT_PREF_KEYS = { R.string.notification_slot_compact_0_key, @@ -152,7 +180,7 @@ public final class NotificationConstants { if (compactSlot == Integer.MAX_VALUE) { // settings not yet populated, return default values - return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS)); + return new ArrayList<>(SLOT_COMPACT_DEFAULTS); } // a negative value (-1) is set when the user does not want a particular compact slot diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java new file mode 100644 index 000000000..4b1fc417f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java @@ -0,0 +1,125 @@ +package org.schabi.newpipe.player.notification; + +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION; + +import android.content.Intent; +import android.graphics.Bitmap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.Player.RepeatMode; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.ui.PlayerUi; + +public final class NotificationPlayerUi extends PlayerUi { + private boolean foregroundNotificationAlreadyCreated = false; + private final NotificationUtil notificationUtil; + + public NotificationPlayerUi(@NonNull final Player player) { + super(player); + notificationUtil = new NotificationUtil(player); + } + + @Override + public void initPlayer() { + super.initPlayer(); + if (!foregroundNotificationAlreadyCreated) { + notificationUtil.createNotificationAndStartForeground(); + foregroundNotificationAlreadyCreated = true; + } + } + + @Override + public void destroy() { + super.destroy(); + notificationUtil.cancelNotificationAndStopForeground(); + } + + @Override + public void onThumbnailLoaded(@Nullable final Bitmap bitmap) { + super.onThumbnailLoaded(bitmap); + notificationUtil.updateThumbnail(); + } + + @Override + public void onBlocked() { + super.onBlocked(); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + + @Override + public void onPlaying() { + super.onPlaying(); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + + @Override + public void onBuffering() { + super.onBuffering(); + if (notificationUtil.shouldUpdateBufferingSlot()) { + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + } + + @Override + public void onPaused() { + super.onPaused(); + + // Remove running notification when user does not want minimization to background or popup + if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE + && player.videoPlayerSelected()) { + notificationUtil.cancelNotificationAndStopForeground(); + } else { + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + + @Override + public void onCompleted() { + super.onCompleted(); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + + @Override + public void onRepeatModeChanged(@RepeatMode final int repeatMode) { + super.onRepeatModeChanged(repeatMode); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + + @Override + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) { + notificationUtil.createNotificationIfNeededAndUpdate(true); + } + } + + @Override + public void onMetadataChanged(@NonNull final StreamInfo info) { + super.onMetadataChanged(info); + notificationUtil.createNotificationIfNeededAndUpdate(true); + } + + @Override + public void onPlayQueueEdited() { + super.onPlayQueueEdited(); + notificationUtil.createNotificationIfNeededAndUpdate(false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java similarity index 69% rename from app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java index 948343be2..3488ec61e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java @@ -1,16 +1,15 @@ -package org.schabi.newpipe.player; +package org.schabi.newpipe.player.notification; import android.annotation.SuppressLint; import android.app.PendingIntent; -import android.app.Service; import android.content.Intent; import android.content.pm.ServiceInfo; import android.graphics.Bitmap; -import android.graphics.Matrix; import android.os.Build; import android.util.Log; import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; @@ -20,48 +19,45 @@ import androidx.core.content.ContextCompat; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.util.NavigationHelper; import java.util.List; +import java.util.Objects; +import java.util.Optional; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static androidx.media.app.NotificationCompat.MediaStyle; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; -import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE; /** * This is a utility class for player notifications. - * - * @author cool-student */ public final class NotificationUtil { private static final String TAG = NotificationUtil.class.getSimpleName(); private static final boolean DEBUG = Player.DEBUG; private static final int NOTIFICATION_ID = 123789; - @Nullable private static NotificationUtil instance = null; - @NotificationConstants.Action private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone(); private NotificationManagerCompat notificationManager; private NotificationCompat.Builder notificationBuilder; - private NotificationUtil() { - } + private final Player player; - public static NotificationUtil getInstance() { - if (instance == null) { - instance = new NotificationUtil(); - } - return instance; + public NotificationUtil(final Player player) { + this.player = player; } @@ -72,20 +68,31 @@ public final class NotificationUtil { /** * Creates the notification if it does not exist already and recreates it if forceRecreate is * true. Updates the notification with the data in the player. - * @param player the player currently open, to take data from * @param forceRecreate whether to force the recreation of the notification even if it already * exists */ - synchronized void createNotificationIfNeededAndUpdate(final Player player, - final boolean forceRecreate) { + public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) { if (forceRecreate || notificationBuilder == null) { - notificationBuilder = createNotification(player); + notificationBuilder = createNotification(); } - updateNotification(player); + updateNotification(); notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } - private synchronized NotificationCompat.Builder createNotification(final Player player) { + public synchronized void updateThumbnail() { + if (notificationBuilder != null) { + if (DEBUG) { + Log.d(TAG, "updateThumbnail() called with thumbnail = [" + Integer.toHexString( + Optional.ofNullable(player.getThumbnail()).map(Objects::hashCode).orElse(0)) + + "], title = [" + player.getVideoTitle() + "]"); + } + + setLargeIcon(notificationBuilder); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + } + + private synchronized NotificationCompat.Builder createNotification() { if (DEBUG) { Log.d(TAG, "createNotification()"); } @@ -94,7 +101,7 @@ public final class NotificationUtil { new NotificationCompat.Builder(player.getContext(), player.getContext().getString(R.string.notification_channel_id)); - initializeNotificationSlots(player); + initializeNotificationSlots(); // count the number of real slots, to make sure compact slots indices are not out of bound int nonNothingSlotCount = 5; @@ -108,14 +115,15 @@ public final class NotificationUtil { // build the compact slot indices array (need code to convert from Integer... because Java) final List compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( player.getContext(), player.getPrefs(), nonNothingSlotCount); - final int[] compactSlots = new int[compactSlotList.size()]; - for (int i = 0; i < compactSlotList.size(); i++) { - compactSlots[i] = compactSlotList.get(i); - } + final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray(); - builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(player.getMediaSessionManager().getSessionToken()) - .setShowActionsInCompactView(compactSlots)) + final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots); + player.UIs() + .get(MediaSessionPlayerUi.class) + .flatMap(MediaSessionPlayerUi::getSessionToken) + .ifPresent(mediaStyle::setMediaSession); + + builder.setStyle(mediaStyle) .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_TRANSPORT) @@ -128,35 +136,33 @@ public final class NotificationUtil { .setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); + // set the initial value for the video thumbnail, updatable with updateNotificationThumbnail + setLargeIcon(builder); + return builder; } /** * Updates the notification builder and the button icons depending on the playback state. - * @param player the player currently open, to take data from */ - private synchronized void updateNotification(final Player player) { + private synchronized void updateNotification() { if (DEBUG) { Log.d(TAG, "updateNotification()"); } // also update content intent, in case the user switched players notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(), - NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT)); + NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT)); notificationBuilder.setContentTitle(player.getVideoTitle()); notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setTicker(player.getVideoTitle()); - updateActions(notificationBuilder, player); - final boolean showThumbnail = player.getPrefs().getBoolean( - player.getContext().getString(R.string.show_thumbnail_key), true); - if (showThumbnail) { - setLargeIcon(notificationBuilder, player); - } + + updateActions(notificationBuilder); } @SuppressLint("RestrictedApi") - boolean shouldUpdateBufferingSlot() { + public boolean shouldUpdateBufferingSlot() { if (notificationBuilder == null) { // if there is no notification active, there is no point in updating it return false; @@ -174,22 +180,22 @@ public final class NotificationUtil { } - void createNotificationAndStartForeground(final Player player, final Service service) { + public void createNotificationAndStartForeground() { if (notificationBuilder == null) { - notificationBuilder = createNotification(player); + notificationBuilder = createNotification(); } - updateNotification(player); + updateNotification(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - service.startForeground(NOTIFICATION_ID, notificationBuilder.build(), + player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); } else { - service.startForeground(NOTIFICATION_ID, notificationBuilder.build()); + player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build()); } } - void cancelNotificationAndStopForeground(final Service service) { - ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE); + public void cancelNotificationAndStopForeground() { + ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE); if (notificationManager != null) { notificationManager.cancel(NOTIFICATION_ID); @@ -203,7 +209,7 @@ public final class NotificationUtil { // ACTIONS ///////////////////////////////////////////////////// - private void initializeNotificationSlots(final Player player) { + private void initializeNotificationSlots() { for (int i = 0; i < 5; ++i) { notificationSlots[i] = player.getPrefs().getInt( player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]), @@ -212,17 +218,16 @@ public final class NotificationUtil { } @SuppressLint("RestrictedApi") - private void updateActions(final NotificationCompat.Builder builder, final Player player) { + private void updateActions(final NotificationCompat.Builder builder) { builder.mActions.clear(); for (int i = 0; i < 5; ++i) { - addAction(builder, player, notificationSlots[i]); + addAction(builder, notificationSlots[i]); } } private void addAction(final NotificationCompat.Builder builder, - final Player player, @NotificationConstants.Action final int slot) { - final NotificationCompat.Action action = getAction(player, slot); + final NotificationCompat.Action action = getAction(slot); if (action != null) { builder.addAction(action); } @@ -230,41 +235,40 @@ public final class NotificationUtil { @Nullable private NotificationCompat.Action getAction( - final Player player, @NotificationConstants.Action final int selectedAction) { final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; switch (selectedAction) { case NotificationConstants.PREVIOUS: - return getAction(player, baseActionIcon, + return getAction(baseActionIcon, R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); case NotificationConstants.NEXT: - return getAction(player, baseActionIcon, + return getAction(baseActionIcon, R.string.exo_controls_next_description, ACTION_PLAY_NEXT); case NotificationConstants.REWIND: - return getAction(player, baseActionIcon, + return getAction(baseActionIcon, R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); case NotificationConstants.FORWARD: - return getAction(player, baseActionIcon, + return getAction(baseActionIcon, R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); case NotificationConstants.SMART_REWIND_PREVIOUS: if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { - return getAction(player, R.drawable.exo_notification_previous, + return getAction(R.drawable.exo_notification_previous, R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); } else { - return getAction(player, R.drawable.exo_controls_rewind, + return getAction(R.drawable.exo_controls_rewind, R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); } case NotificationConstants.SMART_FORWARD_NEXT: if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { - return getAction(player, R.drawable.exo_notification_next, + return getAction(R.drawable.exo_notification_next, R.string.exo_controls_next_description, ACTION_PLAY_NEXT); } else { - return getAction(player, R.drawable.exo_controls_fastforward, + return getAction(R.drawable.exo_controls_fastforward, R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); } @@ -278,44 +282,45 @@ public final class NotificationUtil { null); } + // fallthrough case NotificationConstants.PLAY_PAUSE: if (player.getCurrentState() == Player.STATE_COMPLETED) { - return getAction(player, R.drawable.ic_replay, + return getAction(R.drawable.ic_replay, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else if (player.isPlaying() || player.getCurrentState() == Player.STATE_PREFLIGHT || player.getCurrentState() == Player.STATE_BLOCKED || player.getCurrentState() == Player.STATE_BUFFERING) { - return getAction(player, R.drawable.exo_notification_pause, + return getAction(R.drawable.exo_notification_pause, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else { - return getAction(player, R.drawable.exo_notification_play, + return getAction(R.drawable.exo_notification_play, R.string.exo_controls_play_description, ACTION_PLAY_PAUSE); } case NotificationConstants.REPEAT: if (player.getRepeatMode() == REPEAT_MODE_ALL) { - return getAction(player, R.drawable.exo_media_action_repeat_all, + return getAction(R.drawable.exo_media_action_repeat_all, R.string.exo_controls_repeat_all_description, ACTION_REPEAT); } else if (player.getRepeatMode() == REPEAT_MODE_ONE) { - return getAction(player, R.drawable.exo_media_action_repeat_one, + return getAction(R.drawable.exo_media_action_repeat_one, R.string.exo_controls_repeat_one_description, ACTION_REPEAT); } else /* player.getRepeatMode() == REPEAT_MODE_OFF */ { - return getAction(player, R.drawable.exo_media_action_repeat_off, + return getAction(R.drawable.exo_media_action_repeat_off, R.string.exo_controls_repeat_off_description, ACTION_REPEAT); } case NotificationConstants.SHUFFLE: if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) { - return getAction(player, R.drawable.exo_controls_shuffle_on, + return getAction(R.drawable.exo_controls_shuffle_on, R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); } else { - return getAction(player, R.drawable.exo_controls_shuffle_off, + return getAction(R.drawable.exo_controls_shuffle_off, R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE); } case NotificationConstants.CLOSE: - return getAction(player, R.drawable.ic_close, + return getAction(R.drawable.ic_close, R.string.close, ACTION_CLOSE); case NotificationConstants.NOTHING: @@ -325,8 +330,7 @@ public final class NotificationUtil { } } - private NotificationCompat.Action getAction(final Player player, - @DrawableRes final int drawable, + private NotificationCompat.Action getAction(@DrawableRes final int drawable, @StringRes final int title, final String intentAction) { return new NotificationCompat.Action(drawable, player.getContext().getString(title), @@ -334,7 +338,7 @@ public final class NotificationUtil { new Intent(intentAction), FLAG_UPDATE_CURRENT)); } - private Intent getIntentForNotification(final Player player) { + private Intent getIntentForNotification() { if (player.audioPlayerSelected() || player.popupPlayerSelected()) { // Means we play in popup or audio only. Let's show the play queue return NavigationHelper.getPlayQueueActivityIntent(player.getContext()); @@ -354,28 +358,34 @@ public final class NotificationUtil { // BITMAP ///////////////////////////////////////////////////// - private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) { + private void setLargeIcon(final NotificationCompat.Builder builder) { + final boolean showThumbnail = player.getPrefs().getBoolean( + player.getContext().getString(R.string.show_thumbnail_key), true); + final Bitmap thumbnail = player.getThumbnail(); + if (thumbnail == null || !showThumbnail) { + // since the builder is reused, make sure the thumbnail is unset if there is not one + builder.setLargeIcon(null); + return; + } + final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean( player.getContext().getString(R.string.scale_to_square_image_in_notifications_key), false); if (scaleImageToSquareAspectRatio) { - builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); + builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail)); } else { - builder.setLargeIcon(player.getThumbnail()); + builder.setLargeIcon(thumbnail); } } - private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) { - return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth()); - } - - private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) { - final int width = bitmap.getWidth(); - final int height = bitmap.getHeight(); - final float scaleWidth = ((float) newWidth) / width; - final float scaleHeight = ((float) newHeight) / height; - final Matrix matrix = new Matrix(); - matrix.postScale(scaleWidth, scaleHeight); - return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false); + private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) { + // Find the smaller dimension and then take a center portion of the image that + // has that size. + final int w = bitmap.getWidth(); + final int h = bitmap.getHeight(); + final int dstSize = Math.min(w, h); + final int x = (w - dstSize) / 2; + final int y = (h - dstSize) / 2; + return Bitmap.createBitmap(bitmap, x, y, dstSize, dstSize); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java deleted file mode 100644 index ee0a6f118..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.schabi.newpipe.player.playback; - -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.media.MediaDescriptionCompat; -import android.support.v4.media.MediaMetadataCompat; - -import org.schabi.newpipe.player.Player; -import org.schabi.newpipe.player.mediasession.MediaSessionCallback; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; - -public class PlayerMediaSession implements MediaSessionCallback { - private final Player player; - - public PlayerMediaSession(final Player player) { - this.player = player; - } - - @Override - public void playPrevious() { - player.playPrevious(); - } - - @Override - public void playNext() { - player.playNext(); - } - - @Override - public void playItemAtIndex(final int index) { - if (player.getPlayQueue() == null) { - return; - } - player.selectQueueItem(player.getPlayQueue().getItem(index)); - } - - @Override - public int getCurrentPlayingIndex() { - if (player.getPlayQueue() == null) { - return -1; - } - return player.getPlayQueue().getIndex(); - } - - @Override - public int getQueueSize() { - if (player.getPlayQueue() == null) { - return -1; - } - return player.getPlayQueue().size(); - } - - @Override - public MediaDescriptionCompat getQueueMetadata(final int index) { - if (player.getPlayQueue() == null) { - return null; - } - final PlayQueueItem item = player.getPlayQueue().getItem(index); - if (item == null) { - return null; - } - - final MediaDescriptionCompat.Builder descriptionBuilder - = new MediaDescriptionCompat.Builder() - .setMediaId(String.valueOf(index)) - .setTitle(item.getTitle()) - .setSubtitle(item.getUploader()); - - // set additional metadata for A2DP/AVRCP - final Bundle additionalMetadata = new Bundle(); - additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle()); - additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader()); - additionalMetadata - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000); - additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1); - additionalMetadata - .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size()); - descriptionBuilder.setExtras(additionalMetadata); - - final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); - if (thumbnailUri != null) { - descriptionBuilder.setIconUri(thumbnailUri); - } - - return descriptionBuilder.build(); - } - - @Override - public void play() { - player.play(); - // hide the player controls even if the play command came from the media session - player.hideControls(0, 0); - } - - @Override - public void pause() { - player.pause(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java index 5d67e6967..da6cb36d4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java @@ -4,7 +4,7 @@ import android.content.Context; import android.view.SurfaceHolder; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.video.DummySurface; +import com.google.android.exoplayer2.video.PlaceholderSurface; /** * Prevent error message: 'Unrecoverable player error occurred' @@ -26,7 +26,7 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback { private final Context context; private final Player player; - private DummySurface dummySurface; + private PlaceholderSurface placeholderSurface; public SurfaceHolderCallback(final Context context, final Player player) { this.context = context; @@ -47,16 +47,16 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback { @Override public void surfaceDestroyed(final SurfaceHolder holder) { - if (dummySurface == null) { - dummySurface = DummySurface.newInstanceV17(context, false); + if (placeholderSurface == null) { + placeholderSurface = PlaceholderSurface.newInstanceV17(context, false); } - player.setVideoSurface(dummySurface); + player.setVideoSurface(placeholderSurface); } public void release() { - if (dummySurface != null) { - dummySurface.release(); - dummySurface = null; + if (placeholderSurface != null) { + placeholderSurface.release(); + placeholderSurface = null; } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index df2747c3b..e51ee4720 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -82,7 +82,7 @@ abstract class AbstractInfoPlayQueue> public void onError(@NonNull final Throwable e) { Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e); isComplete = true; - append(); // Notify change + notifyChange(); } }; } @@ -117,7 +117,7 @@ abstract class AbstractInfoPlayQueue> public void onError(@NonNull final Throwable e) { Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e); isComplete = true; - append(); // Notify change + notifyChange(); } }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index f46c9d72f..edf5a771c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -16,7 +16,6 @@ import org.schabi.newpipe.player.playqueue.events.SelectEvent; import java.io.Serializable; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -258,13 +257,10 @@ public abstract class PlayQueue implements Serializable { } /** - * Appends the given {@link PlayQueueItem}s to the current play queue. - * - * @see #append(List items) - * @param items {@link PlayQueueItem}s to append + * Notifies that a change has occurred. */ - public synchronized void append(@NonNull final PlayQueueItem... items) { - append(Arrays.asList(items)); + public synchronized void notifyChange() { + broadcast(new AppendEvent(0)); } /** diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java index b283e105e..6e2792d4f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.player.playqueue; +import androidx.annotation.NonNull; +import androidx.core.math.MathUtils; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; @@ -16,18 +18,21 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC public abstract void onSwiped(int index); @Override - public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, final int viewSize, - final int viewSizeOutOfBounds, final int totalSize, + public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView, + final int viewSize, + final int viewSizeOutOfBounds, + final int totalSize, final long msSinceStartScroll) { final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, - Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); + final int clampedAbsVelocity = MathUtils.clamp(Math.abs(standardSpeed), + MINIMUM_INITIAL_DRAG_VELOCITY, MAXIMUM_INITIAL_DRAG_VELOCITY); return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); } @Override - public boolean onMove(final RecyclerView recyclerView, final RecyclerView.ViewHolder source, + public boolean onMove(@NonNull final RecyclerView recyclerView, + final RecyclerView.ViewHolder source, final RecyclerView.ViewHolder target) { if (source.getItemViewType() != target.getItemViewType()) { return false; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java index 527e80470..e51173214 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java @@ -4,20 +4,19 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public final class SinglePlayQueue extends PlayQueue { public SinglePlayQueue(final StreamInfoItem item) { - super(0, Collections.singletonList(new PlayQueueItem(item))); + super(0, List.of(new PlayQueueItem(item))); } public SinglePlayQueue(final StreamInfo info) { - super(0, Collections.singletonList(new PlayQueueItem(info))); + super(0, List.of(new PlayQueueItem(info))); } public SinglePlayQueue(final StreamInfo info, final long startPosition) { - super(0, Collections.singletonList(new PlayQueueItem(info))); + super(0, List.of(new PlayQueueItem(info))); getItem().setRecoveryPosition(startPosition); } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index 34e7e9bd1..ead127250 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -172,9 +172,10 @@ public interface PlaybackResolver extends Resolver { try { final StreamInfoTag tag = StreamInfoTag.of(info); if (!info.getHlsUrl().isEmpty()) { - return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); + return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.CONTENT_TYPE_HLS, tag); } else if (!info.getDashMpdUrl().isEmpty()) { - return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); + return buildLiveMediaSource( + dataSource, info.getDashMpdUrl(), C.CONTENT_TYPE_DASH, tag); } } catch (final Exception e) { Log.w(TAG, "Error when generating live media source, falling back to standard sources", @@ -190,17 +191,17 @@ public interface PlaybackResolver extends Resolver { final MediaItemTag metadata) throws ResolverException { final MediaSource.Factory factory; switch (type) { - case C.TYPE_SS: + case C.CONTENT_TYPE_SS: factory = dataSource.getLiveSsMediaSourceFactory(); break; - case C.TYPE_DASH: + case C.CONTENT_TYPE_DASH: factory = dataSource.getLiveDashMediaSourceFactory(); break; - case C.TYPE_HLS: + case C.CONTENT_TYPE_HLS: factory = dataSource.getLiveHlsMediaSourceFactory(); break; - case C.TYPE_OTHER: - case C.TYPE_RTSP: + case C.CONTENT_TYPE_OTHER: + case C.CONTENT_TYPE_RTSP: default: throw new ResolverException("Unsupported type: " + type); } diff --git a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java index 54d11da83..43d89055c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java @@ -8,13 +8,13 @@ import android.widget.ImageView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; +import androidx.core.math.MathUtils; import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.util.DeviceUtils; import java.lang.annotation.Retention; -import java.util.Objects; import java.util.Optional; import java.util.function.IntSupplier; @@ -79,19 +79,14 @@ public final class SeekbarPreviewThumbnailHelper { // Resize original bitmap try { - Objects.requireNonNull(srcBitmap); - final int srcWidth = srcBitmap.getWidth() > 0 ? srcBitmap.getWidth() : 1; - final int newWidth = Math.max( - Math.min( - // Use 1/4 of the width for the preview - Math.round(baseViewWidthSupplier.getAsInt() / 4f), - // Scaling more than that factor looks really pixelated -> max - Math.round(srcWidth * 2.5f) - ), - // Min width = 10dp - DeviceUtils.dpToPx(10, context) - ); + final int newWidth = MathUtils.clamp( + // Use 1/4 of the width for the preview + Math.round(baseViewWidthSupplier.getAsInt() / 4f), + // But have a min width of 10dp + DeviceUtils.dpToPx(10, context), + // And scaling more than that factor looks really pixelated -> max + Math.round(srcWidth * 2.5f)); final float scaleFactor = (float) newWidth / srcWidth; final int newHeight = (int) (srcBitmap.getHeight() * scaleFactor); diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java new file mode 100644 index 000000000..81dc954d1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java @@ -0,0 +1,981 @@ +package org.schabi.newpipe.player.ui; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static org.schabi.newpipe.MainActivity.DEBUG; +import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.player.Player.STATE_COMPLETED; +import static org.schabi.newpipe.player.Player.STATE_PAUSED; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; +import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Bitmap; +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; +import android.view.ViewParent; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.exoplayer2.video.VideoSize; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlayerBinding; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamSegment; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.info_list.StreamSegmentAdapter; +import org.schabi.newpipe.ktx.AnimationType; +import org.schabi.newpipe.local.dialog.PlaylistDialog; +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.gesture.BasePlayerGestureListener; +import org.schabi.newpipe.player.gesture.MainPlayerGestureListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.external_communication.KoreUtils; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener { + private static final String TAG = MainPlayerUi.class.getSimpleName(); + + // see the Javadoc of calculateMaxEndScreenThumbnailHeight for information + private static final int DETAIL_ROOT_MINIMUM_HEIGHT = 85; // dp + private static final int DETAIL_TITLE_TEXT_SIZE_TV = 16; // sp + private static final int DETAIL_TITLE_TEXT_SIZE_TABLET = 15; // sp + + private boolean isFullscreen = false; + private boolean isVerticalVideo = false; + private boolean fragmentIsVisible = false; + + private ContentObserver settingsContentObserver; + + private PlayQueueAdapter playQueueAdapter; + private StreamSegmentAdapter segmentAdapter; + private boolean isQueueVisible = false; + private boolean areSegmentsVisible = false; + + // fullscreen player + private ItemTouchHelper itemTouchHelper; + + + /*////////////////////////////////////////////////////////////////////////// + // Constructor, setup, destroy + //////////////////////////////////////////////////////////////////////////*/ + //region Constructor, setup, destroy + + public MainPlayerUi(@NonNull final Player player, + @NonNull final PlayerBinding playerBinding) { + super(player, playerBinding); + } + + /** + * Open fullscreen on tablets where the option to have the main player start automatically in + * fullscreen mode is on. Rotating the device to landscape is already done in {@link + * VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's + * enough for phones, but not for tablets since the mini player can be also shown in landscape. + */ + private void directlyOpenFullscreenIfNeeded() { + if (PlayerHelper.isStartMainPlayerFullscreenEnabled(player.getService()) + && DeviceUtils.isTablet(player.getService()) + && PlayerHelper.globalScreenOrientationLocked(player.getService())) { + player.getFragmentListener().ifPresent( + PlayerServiceEventListener::onScreenRotationButtonClicked); + } + } + + @Override + public void setupAfterIntent() { + // needed for tablets, check the function for a better explanation + directlyOpenFullscreenIfNeeded(); + + super.setupAfterIntent(); + + initVideoPlayer(); + // Android TV: without it focus will frame the whole player + binding.playPauseButton.requestFocus(); + + // Note: This is for automatically playing (when "Resume playback" is off), see #6179 + if (player.getPlayWhenReady()) { + player.play(); + } else { + player.pause(); + } + } + + @Override + BasePlayerGestureListener buildGestureListener() { + return new MainPlayerGestureListener(this); + } + + @Override + protected void initListeners() { + super.initListeners(); + + binding.queueButton.setOnClickListener(v -> onQueueClicked()); + binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked()); + + binding.addToPlaylistButton.setOnClickListener(v -> + getParentActivity().map(FragmentActivity::getSupportFragmentManager) + .ifPresent(fragmentManager -> + PlaylistDialog.showForPlayQueue(player, fragmentManager))); + + settingsContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(final boolean selfChange) { + setupScreenRotationButton(); + } + }; + context.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); + + binding.getRoot().addOnLayoutChangeListener(this); + } + + @Override + protected void deinitListeners() { + super.deinitListeners(); + + binding.queueButton.setOnClickListener(null); + binding.segmentsButton.setOnClickListener(null); + binding.addToPlaylistButton.setOnClickListener(null); + + context.getContentResolver().unregisterContentObserver(settingsContentObserver); + + binding.getRoot().removeOnLayoutChangeListener(this); + } + + @Override + public void initPlayback() { + super.initPlayback(); + + if (playQueueAdapter != null) { + playQueueAdapter.dispose(); + } + playQueueAdapter = new PlayQueueAdapter(context, + Objects.requireNonNull(player.getPlayQueue())); + segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener()); + } + + @Override + public void removeViewFromParent() { + // view was added to fragment + final ViewParent parent = binding.getRoot().getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(binding.getRoot()); + } + } + + @Override + public void destroy() { + super.destroy(); + + // Exit from fullscreen when user closes the player via notification + if (isFullscreen) { + toggleFullscreen(); + } + + removeViewFromParent(); + } + + @Override + public void destroyPlayer() { + super.destroyPlayer(); + + if (playQueueAdapter != null) { + playQueueAdapter.unsetSelectedListener(); + playQueueAdapter.dispose(); + } + } + + @Override + public void smoothStopForImmediateReusing() { + super.smoothStopForImmediateReusing(); + // Android TV will handle back button in case controls will be visible + // (one more additional unneeded click while the player is hidden) + hideControls(0, 0); + closeItemsList(); + } + + private void initVideoPlayer() { + // restore last resize mode + setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(player)); + binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + + @Override + protected void setupElementsVisibility() { + super.setupElementsVisibility(); + + closeItemsList(); + showHideKodiButton(); + binding.fullScreenButton.setVisibility(View.GONE); + setupScreenRotationButton(); + binding.resizeTextView.setVisibility(View.VISIBLE); + binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); + binding.moreOptionsButton.setVisibility(View.VISIBLE); + binding.topControls.setOrientation(LinearLayout.VERTICAL); + binding.primaryControls.getLayoutParams().width = MATCH_PARENT; + binding.secondaryControls.setVisibility(View.INVISIBLE); + binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context, + R.drawable.ic_expand_more)); + binding.share.setVisibility(View.VISIBLE); + binding.openInBrowser.setVisibility(View.VISIBLE); + binding.switchMute.setVisibility(View.VISIBLE); + binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); + // Top controls have a large minHeight which is allows to drag the player + // down in fullscreen mode (just larger area to make easy to locate by finger) + binding.topControls.setClickable(true); + binding.topControls.setFocusable(true); + + binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + } + + @Override + protected void setupElementsSize(final Resources resources) { + setupElementsSize( + resources.getDimensionPixelSize(R.dimen.player_main_buttons_min_width), + resources.getDimensionPixelSize(R.dimen.player_main_top_padding), + resources.getDimensionPixelSize(R.dimen.player_main_controls_padding), + resources.getDimensionPixelSize(R.dimen.player_main_buttons_padding) + ); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast receiver + //////////////////////////////////////////////////////////////////////////*/ + //region Broadcast receiver + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { + // Close it because when changing orientation from portrait + // (in fullscreen mode) the size of queue layout can be larger than the screen size + closeItemsList(); + } else if (ACTION_PLAY_PAUSE.equals(intent.getAction())) { + // Ensure that we have audio-only stream playing when a user + // started to play from notification's play button from outside of the app + if (!fragmentIsVisible) { + onFragmentStopped(); + } + } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED.equals(intent.getAction())) { + fragmentIsVisible = false; + onFragmentStopped(); + } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED.equals(intent.getAction())) { + // Restore video source when user returns to the fragment + fragmentIsVisible = true; + player.useVideoSource(true); + + // When a user returns from background, the system UI will always be shown even if + // controls are invisible: hide it in that case + if (!isControlsVisible()) { + hideSystemUIIfNeeded(); + } + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Fragment binding + //////////////////////////////////////////////////////////////////////////*/ + //region Fragment binding + + @Override + public void onFragmentListenerSet() { + super.onFragmentListenerSet(); + fragmentIsVisible = true; + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait + if (!isFullscreen) { + binding.playbackControlRoot.setPadding(0, 0, 0, 0); + } + binding.itemsListPanel.setPadding(0, 0, 0, 0); + player.getFragmentListener().ifPresent(PlayerServiceEventListener::onViewCreated); + } + + /** + * This will be called when a user goes to another app/activity, turns off a screen. + * We don't want to interrupt playback and don't want to see notification so + * next lines of code will enable audio-only playback only if needed + */ + private void onFragmentStopped() { + if (player.isPlaying() || player.isLoading()) { + switch (getMinimizeOnExitAction(context)) { + case MINIMIZE_ON_EXIT_MODE_BACKGROUND: + player.useVideoSource(false); + break; + case MINIMIZE_ON_EXIT_MODE_POPUP: + getParentActivity().ifPresent(activity -> { + player.setRecovery(); + NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true); + }); + break; + case MINIMIZE_ON_EXIT_MODE_NONE: default: + player.pause(); + break; + } + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Playback states + //////////////////////////////////////////////////////////////////////////*/ + //region Playback states + + @Override + public void onUpdateProgress(final int currentProgress, + final int duration, + final int bufferPercent) { + super.onUpdateProgress(currentProgress, duration, bufferPercent); + + if (areSegmentsVisible) { + segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress)); + } + if (isQueueVisible) { + updateQueueTime(currentProgress); + } + } + + @Override + public void onPlaying() { + super.onPlaying(); + checkLandscape(); + } + + @Override + public void onCompleted() { + super.onCompleted(); + if (isFullscreen) { + toggleFullscreen(); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Controls showing / hiding + //////////////////////////////////////////////////////////////////////////*/ + //region Controls showing / hiding + + @Override + protected void showOrHideButtons() { + super.showOrHideButtons(); + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue == null) { + return; + } + + final boolean showQueue = playQueue.getStreams().size() > 1; + final boolean showSegment = !player.getCurrentStreamInfo() + .map(StreamInfo::getStreamSegments) + .map(List::isEmpty) + .orElse(/*no stream info=*/true); + + binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); + binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f); + binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE); + binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f); + } + + @Override + public void showSystemUIPartially() { + if (isFullscreen) { + getParentActivity().map(Activity::getWindow).ifPresent(window -> { + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + window.getDecorView().setSystemUiVisibility(visibility); + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + }); + } + } + + @Override + public void hideSystemUIIfNeeded() { + player.getFragmentListener().ifPresent(PlayerServiceEventListener::hideSystemUiIfNeeded); + } + + /** + * Calculate the maximum allowed height for the {@link R.id.endScreen} + * to prevent it from enlarging the player. + *

+ * The calculating follows these rules: + *

    + *
  • + * Show at least stream title and content creator on TVs and tablets when in landscape + * (always the case for TVs) and not in fullscreen mode. This requires to have at least + * {@link #DETAIL_ROOT_MINIMUM_HEIGHT} free space for {@link R.id.detail_root} and + * additional space for the stream title text size ({@link R.id.detail_title_root_layout}). + * The text size is {@link #DETAIL_TITLE_TEXT_SIZE_TABLET} on tablets and + * {@link #DETAIL_TITLE_TEXT_SIZE_TV} on TVs, see {@link R.id.titleTextView}. + *
  • + *
  • + * Otherwise, the max thumbnail height is the screen height. + *
  • + *
+ * + * @param bitmap the bitmap that needs to be resized to fit the end screen + * @return the maximum height for the end screen thumbnail + */ + @Override + protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) { + final int screenHeight = context.getResources().getDisplayMetrics().heightPixels; + + if (DeviceUtils.isTv(context) && !isFullscreen()) { + final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context) + + DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TV, context); + return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight); + } else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) { + final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context) + + DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TABLET, context); + return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight); + } else { // fullscreen player: max height is the device height + return Math.min(bitmap.getHeight(), screenHeight); + } + } + + private void showHideKodiButton() { + // show kodi button if it supports the current service and it is enabled in settings + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null + && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) + ? View.VISIBLE : View.GONE); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Captions (text tracks) + //////////////////////////////////////////////////////////////////////////*/ + //region Captions (text tracks) + + @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); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + //region Gestures + + @SuppressWarnings("checkstyle:ParameterNumber") + @Override + public void onLayoutChange(final View view, final int l, final int t, final int r, final int b, + final int ol, final int ot, final int or, final int ob) { + if (l != ol || t != ot || r != or || b != ob) { + // Use a smaller value to be consistent across screen orientations, and to make usage + // easier. Multiply by 3/4 to ensure the user does not need to move the finger up to the + // screen border, in order to reach the maximum volume/brightness. + final int width = r - l; + final int height = b - t; + final int min = Math.min(width, height); + final int maxGestureLength = (int) (min * 0.75); + + if (DEBUG) { + Log.d(TAG, "maxGestureLength = " + maxGestureLength); + } + + binding.volumeProgressBar.setMax(maxGestureLength); + binding.brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); + binding.itemsListPanel.getLayoutParams().height = + height - binding.itemsListPanel.getTop(); + } + } + + private void setInitialGestureValues() { + if (player.getAudioReactor() != null) { + final float currentVolumeNormalized = (float) player.getAudioReactor().getVolume() + / player.getAudioReactor().getMaxVolume(); + binding.volumeProgressBar.setProgress( + (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Play queue, segments and streams + //////////////////////////////////////////////////////////////////////////*/ + //region Play queue, segments and streams + + @Override + public void onMetadataChanged(@NonNull final StreamInfo info) { + super.onMetadataChanged(info); + showHideKodiButton(); + if (areSegmentsVisible) { + if (segmentAdapter.setItems(info)) { + final int adapterPosition = getNearestStreamSegmentPosition( + player.getExoPlayer().getCurrentPosition()); + segmentAdapter.selectSegmentAt(adapterPosition); + binding.itemsList.scrollToPosition(adapterPosition); + } else { + closeItemsList(); + } + } + } + + @Override + public void onPlayQueueEdited() { + super.onPlayQueueEdited(); + showOrHideButtons(); + } + + private void onQueueClicked() { + isQueueVisible = true; + + hideSystemUIIfNeeded(); + buildQueue(); + + binding.itemsListHeaderTitle.setVisibility(View.GONE); + binding.itemsListHeaderDuration.setVisibility(View.VISIBLE); + binding.shuffleButton.setVisibility(View.VISIBLE); + binding.repeatButton.setVisibility(View.VISIBLE); + binding.addToPlaylistButton.setVisibility(View.VISIBLE); + + hideControls(0, 0); + binding.itemsListPanel.requestFocus(); + animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION, + AnimationType.SLIDE_AND_ALPHA); + + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue != null) { + binding.itemsList.scrollToPosition(playQueue.getIndex()); + } + + updateQueueTime((int) player.getExoPlayer().getCurrentPosition()); + } + + private void buildQueue() { + binding.itemsList.setAdapter(playQueueAdapter); + binding.itemsList.setClickable(true); + binding.itemsList.setLongClickable(true); + + binding.itemsList.clearOnScrollListeners(); + binding.itemsList.addOnScrollListener(getQueueScrollListener()); + + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(binding.itemsList); + + playQueueAdapter.setSelectedListener(getOnSelectedListener()); + + binding.itemsListClose.setOnClickListener(view -> closeItemsList()); + } + + private void onSegmentsClicked() { + areSegmentsVisible = true; + + hideSystemUIIfNeeded(); + buildSegments(); + + binding.itemsListHeaderTitle.setVisibility(View.VISIBLE); + binding.itemsListHeaderDuration.setVisibility(View.GONE); + binding.shuffleButton.setVisibility(View.GONE); + binding.repeatButton.setVisibility(View.GONE); + binding.addToPlaylistButton.setVisibility(View.GONE); + + hideControls(0, 0); + binding.itemsListPanel.requestFocus(); + animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION, + AnimationType.SLIDE_AND_ALPHA); + + final int adapterPosition = getNearestStreamSegmentPosition( + player.getExoPlayer().getCurrentPosition()); + segmentAdapter.selectSegmentAt(adapterPosition); + binding.itemsList.scrollToPosition(adapterPosition); + } + + private void buildSegments() { + binding.itemsList.setAdapter(segmentAdapter); + binding.itemsList.setClickable(true); + binding.itemsList.setLongClickable(false); + + binding.itemsList.clearOnScrollListeners(); + if (itemTouchHelper != null) { + itemTouchHelper.attachToRecyclerView(null); + } + + player.getCurrentStreamInfo().ifPresent(segmentAdapter::setItems); + + binding.shuffleButton.setVisibility(View.GONE); + binding.repeatButton.setVisibility(View.GONE); + binding.addToPlaylistButton.setVisibility(View.GONE); + binding.itemsListClose.setOnClickListener(view -> closeItemsList()); + } + + public void closeItemsList() { + if (isQueueVisible || areSegmentsVisible) { + isQueueVisible = false; + areSegmentsVisible = false; + + if (itemTouchHelper != null) { + itemTouchHelper.attachToRecyclerView(null); + } + + animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION, + AnimationType.SLIDE_AND_ALPHA, 0, () -> + // Even when queueLayout is GONE it receives touch events + // and ruins normal behavior of the app. This line fixes it + binding.itemsListPanel.setTranslationY( + -binding.itemsListPanel.getHeight() * 5.0f)); + + // clear focus, otherwise a white rectangle remains on top of the player + binding.itemsListClose.clearFocus(); + binding.playPauseButton.requestFocus(); + } + } + + private OnScrollBelowItemsListener getQueueScrollListener() { + return new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue != null && !playQueue.isComplete()) { + playQueue.fetch(); + } else if (binding != null) { + binding.itemsList.clearOnScrollListeners(); + } + } + }; + } + + private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { + return (item, seconds) -> { + segmentAdapter.selectSegment(item); + player.seekTo(seconds * 1000L); + player.triggerProgressUpdate(); + }; + } + + private int getNearestStreamSegmentPosition(final long playbackPosition) { + //noinspection SimplifyOptionalCallChains + if (!player.getCurrentStreamInfo().isPresent()) { + return 0; + } + + int nearestPosition = 0; + final List segments = player.getCurrentStreamInfo() + .get() + .getStreamSegments(); + + for (int i = 0; i < segments.size(); i++) { + if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) { + break; + } + nearestPosition++; + } + return Math.max(0, nearestPosition - 1); + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new PlayQueueItemTouchCallback() { + @Override + public void onMove(final int sourceIndex, final int targetIndex) { + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue != null) { + playQueue.move(sourceIndex, targetIndex); + } + } + + @Override + public void onSwiped(final int index) { + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue != null && index != -1) { + playQueue.remove(index); + } + } + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(final PlayQueueItem item, final View view) { + player.selectQueueItem(item); + } + + @Override + public void held(final PlayQueueItem item, final View view) { + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + @Nullable final AppCompatActivity parentActivity = getParentActivity().orElse(null); + if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) { + openPopupMenu(player.getPlayQueue(), item, view, true, + parentActivity.getSupportFragmentManager(), context); + } + } + + @Override + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } + } + }; + } + + private void updateQueueTime(final int currentTime) { + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue == null) { + return; + } + + final int currentStream = playQueue.getIndex(); + int before = 0; + int after = 0; + + final List streams = playQueue.getStreams(); + final int nStreams = streams.size(); + + for (int i = 0; i < nStreams; i++) { + if (i < currentStream) { + before += streams.get(i).getDuration(); + } else { + after += streams.get(i).getDuration(); + } + } + + before *= 1000; + after *= 1000; + + binding.itemsListHeaderDuration.setText( + String.format("%s/%s", + getTimeString(currentTime + before), + getTimeString(before + after) + )); + } + + @Override + protected boolean isAnyListViewOpen() { + return isQueueVisible || areSegmentsVisible; + } + + @Override + public boolean isFullscreen() { + return isFullscreen; + } + + public boolean isVerticalVideo() { + return isVerticalVideo; + } + + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Click listeners + //////////////////////////////////////////////////////////////////////////*/ + //region Click listeners + + @Override + public void onClick(final View v) { + if (v.getId() == binding.screenRotationButton.getId()) { + // Only if it's not a vertical video or vertical video but in landscape with locked + // orientation a screen orientation can be changed automatically + if (!isVerticalVideo || (isLandscape() && globalScreenOrientationLocked(context))) { + player.getFragmentListener().ifPresent( + PlayerServiceEventListener::onScreenRotationButtonClicked); + } else { + toggleFullscreen(); + } + } + + // call it later since it calls manageControlsAfterOnClick at the end + super.onClick(v); + } + + @Override + protected void onPlaybackSpeedClicked() { + final AppCompatActivity activity = getParentActivity().orElse(null); + if (activity == null) { + return; + } + + PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), + player.getPlaybackSkipSilence(), player::setPlaybackParameters) + .show(activity.getSupportFragmentManager(), null); + } + + @Override + public boolean onLongClick(final View v) { + if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) { + player.getFragmentListener().ifPresent( + PlayerServiceEventListener::onMoreOptionsLongClicked); + hideControls(0, 0); + hideSystemUIIfNeeded(); + return true; + } + return super.onLongClick(v); + } + + @Override + public boolean onKeyDown(final int keyCode) { + if (keyCode == KeyEvent.KEYCODE_SPACE && isFullscreen) { + player.playPause(); + if (player.isPlaying()) { + hideControls(0, 0); + } + return true; + } + return super.onKeyDown(keyCode); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Video size, orientation, fullscreen + //////////////////////////////////////////////////////////////////////////*/ + //region Video size, orientation, fullscreen + + private void setupScreenRotationButton() { + binding.screenRotationButton.setVisibility(globalScreenOrientationLocked(context) + || isVerticalVideo || DeviceUtils.isTablet(context) + ? View.VISIBLE : View.GONE); + binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context, + isFullscreen ? R.drawable.ic_fullscreen_exit + : R.drawable.ic_fullscreen)); + } + + @Override + public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { + super.onVideoSizeChanged(videoSize); + isVerticalVideo = videoSize.width < videoSize.height; + + if (globalScreenOrientationLocked(context) + && isFullscreen + && isLandscape() == isVerticalVideo + && !DeviceUtils.isTv(context) + && !DeviceUtils.isTablet(context)) { + // set correct orientation + player.getFragmentListener().ifPresent( + PlayerServiceEventListener::onScreenRotationButtonClicked); + } + + setupScreenRotationButton(); + } + + public void toggleFullscreen() { + if (DEBUG) { + Log.d(TAG, "toggleFullscreen() called"); + } + final PlayerServiceEventListener fragmentListener = player.getFragmentListener() + .orElse(null); + if (fragmentListener == null || player.exoPlayerIsNull()) { + return; + } + + isFullscreen = !isFullscreen; + if (isFullscreen) { + // Android needs tens milliseconds to send new insets but a user is able to see + // how controls changes it's position from `0` to `nav bar height` padding. + // So just hide the controls to hide this visual inconsistency + hideControls(0, 0); + } else { + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait (open vertical video to reproduce) + binding.playbackControlRoot.setPadding(0, 0, 0, 0); + } + fragmentListener.onFullscreenStateChanged(isFullscreen); + + binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); + setupScreenRotationButton(); + } + + public void checkLandscape() { + // check if landscape is correct + final boolean videoInLandscapeButNotInFullscreen = isLandscape() + && !isFullscreen + && !player.isAudioOnly(); + final boolean notPaused = player.getCurrentState() != STATE_COMPLETED + && player.getCurrentState() != STATE_PAUSED; + + if (videoInLandscapeButNotInFullscreen + && notPaused + && !DeviceUtils.isTablet(context)) { + toggleFullscreen(); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + //region Getters + + public Optional getParentActivity() { + final ViewParent rootParent = binding.getRoot().getParent(); + if (rootParent instanceof ViewGroup) { + final Context activity = ((ViewGroup) rootParent).getContext(); + if (activity instanceof AppCompatActivity) { + return Optional.of((AppCompatActivity) activity); + } + } + return Optional.empty(); + } + + public boolean isLandscape() { + // DisplayMetrics from activity context knows about MultiWindow feature + // while DisplayMetrics from app context doesn't + return DeviceUtils.isLandscape( + getParentActivity().map(Context.class::cast).orElse(player.getService())); + } + //endregion +} diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java new file mode 100644 index 000000000..57e2ec2a2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java @@ -0,0 +1,212 @@ +package org.schabi.newpipe.player.ui; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Tracks; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.video.VideoSize; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.Player; + +import java.util.List; + +/** + * A player UI is a component that can seamlessly connect and disconnect from the {@link Player} and + * provide a user interface of some sort. Try to extend this class instead of adding more code to + * {@link Player}! + */ +public abstract class PlayerUi { + + @NonNull protected final Context context; + @NonNull protected final Player player; + + /** + * @param player the player instance that will be usable throughout the lifetime of this UI; its + * context should already have been initialized + */ + protected PlayerUi(@NonNull final Player player) { + this.context = player.getContext(); + this.player = player; + } + + /** + * @return the player instance this UI was constructed with + */ + @NonNull + public Player getPlayer() { + return player; + } + + + /** + * Called after the player received an intent and processed it. + */ + public void setupAfterIntent() { + } + + /** + * Called right after the exoplayer instance is constructed, or right after this UI is + * constructed if the exoplayer is already available then. Note that the exoplayer instance + * could be built and destroyed multiple times during the lifetime of the player, so this method + * might be called multiple times. + */ + public void initPlayer() { + } + + /** + * Called when playback in the exoplayer is about to start, or right after this UI is + * constructed if the exoplayer and the play queue are already available then. The play queue + * will therefore always be not null. + */ + public void initPlayback() { + } + + /** + * Called when the exoplayer instance is about to be destroyed. Note that the exoplayer instance + * could be built and destroyed multiple times during the lifetime of the player, so this method + * might be called multiple times. Be sure to unset any video surface view or play queue + * listeners! This will also be called when this UI is being discarded, just before {@link + * #destroy()}. + */ + public void destroyPlayer() { + } + + /** + * Called when this UI is being discarded, either because the player is switching to a different + * UI or because the player is shutting down completely. + */ + public void destroy() { + } + + /** + * Called when the player is smooth-stopping, that is, transitioning smoothly to a new play + * queue after the user tapped on a new video stream while a stream was playing in the video + * detail fragment. + */ + public void smoothStopForImmediateReusing() { + } + + /** + * Called when the video detail fragment listener is connected with the player, or right after + * this UI is constructed if the listener is already connected then. + */ + public void onFragmentListenerSet() { + } + + /** + * Broadcasts that the player receives will also be notified to UIs here. If you want to + * register new broadcast actions to receive here, add them to {@link + * Player#setupBroadcastReceiver()}. + * @param intent the broadcast intent received by the player + */ + public void onBroadcastReceived(final Intent intent) { + } + + /** + * Called when stream progress (i.e. the current time in the seekbar) or stream duration change. + * Will surely be called every {@link Player#PROGRESS_LOOP_INTERVAL_MILLIS} while a stream is + * playing. + * @param currentProgress the current progress in milliseconds + * @param duration the duration of the stream being played + * @param bufferPercent the percentage of stream already buffered, see {@link + * com.google.android.exoplayer2.BasePlayer#getBufferedPercentage()} + */ + public void onUpdateProgress(final int currentProgress, + final int duration, + final int bufferPercent) { + } + + public void onPrepared() { + } + + public void onBlocked() { + } + + public void onPlaying() { + } + + public void onBuffering() { + } + + public void onPaused() { + } + + public void onPausedSeek() { + } + + public void onCompleted() { + } + + public void onRepeatModeChanged(@RepeatMode final int repeatMode) { + } + + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + } + + public void onMuteUnmuteChanged(final boolean isMuted) { + } + + /** + * @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks) + * @param currentTracks the available tracks information + */ + public void onTextTracksChanged(@NonNull final Tracks currentTracks) { + } + + /** + * @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged + * @param playbackParameters the new playback parameters + */ + public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { + } + + /** + * @see com.google.android.exoplayer2.Player.Listener#onRenderedFirstFrame + */ + public void onRenderedFirstFrame() { + } + + /** + * @see com.google.android.exoplayer2.text.TextOutput#onCues + * @param cues the cues to pass to the subtitle view + */ + public void onCues(@NonNull final List cues) { + } + + /** + * Called when the stream being played changes. + * @param info the {@link StreamInfo} metadata object, along with data about the selected and + * available video streams (to be used to build the resolution menus, for example) + */ + public void onMetadataChanged(@NonNull final StreamInfo info) { + } + + /** + * Called when the thumbnail for the current metadata was loaded. + * @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an + * error when loading the thumbnail + */ + public void onThumbnailLoaded(@Nullable final Bitmap bitmap) { + } + + /** + * Called when the play queue was edited: a stream was appended, moved or removed. + */ + public void onPlayQueueEdited() { + } + + /** + * @param videoSize the new video size, useful to set the surface aspect ratio + * @see com.google.android.exoplayer2.Player.Listener#onVideoSizeChanged + */ + public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java new file mode 100644 index 000000000..24fec3b8a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java @@ -0,0 +1,90 @@ +package org.schabi.newpipe.player.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +public final class PlayerUiList { + final List playerUis = new ArrayList<>(); + + /** + * Creates a {@link PlayerUiList} starting with the provided player uis. The provided player uis + * will not be prepared like those passed to {@link #addAndPrepare(PlayerUi)}, because when + * the {@link PlayerUiList} constructor is called, the player is still not running and it + * wouldn't make sense to initialize uis then. Instead the player will initialize them by doing + * proper calls to {@link #call(Consumer)}. + * + * @param initialPlayerUis the player uis this list should start with; the order will be kept + */ + public PlayerUiList(final PlayerUi... initialPlayerUis) { + playerUis.addAll(List.of(initialPlayerUis)); + } + + /** + * Adds the provided player ui to the list and calls on it the initialization functions that + * apply based on the current player state. The preparation step needs to be done since when UIs + * are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer + * is already initialized, but we need to notify the newly built UI that the player is ready + * nonetheless. + * @param playerUi the player ui to prepare and add to the list; its {@link + * PlayerUi#getPlayer()} will be used to query information about the player + * state + */ + public void addAndPrepare(final PlayerUi playerUi) { + if (playerUi.getPlayer().getFragmentListener().isPresent()) { + // make sure UIs know whether a service is connected or not + playerUi.onFragmentListenerSet(); + } + + if (!playerUi.getPlayer().exoPlayerIsNull()) { + playerUi.initPlayer(); + if (playerUi.getPlayer().getPlayQueue() != null) { + playerUi.initPlayback(); + } + } + + playerUis.add(playerUi); + } + + /** + * Destroys all matching player UIs and removes them from the list. + * @param playerUiType the class of the player UI to destroy; the {@link + * Class#isInstance(Object)} method will be used, so even subclasses will be + * destroyed and removed + * @param the class type parameter + */ + public void destroyAll(final Class playerUiType) { + playerUis.stream() + .filter(playerUiType::isInstance) + .forEach(playerUi -> { + playerUi.destroyPlayer(); + playerUi.destroy(); + }); + playerUis.removeIf(playerUiType::isInstance); + } + + /** + * @param playerUiType the class of the player UI to return; the {@link + * Class#isInstance(Object)} method will be used, so even subclasses could + * be returned + * @param the class type parameter + * @return the first player UI of the required type found in the list, or an empty {@link + * Optional} otherwise + */ + public Optional get(final Class playerUiType) { + return playerUis.stream() + .filter(playerUiType::isInstance) + .map(playerUiType::cast) + .findFirst(); + } + + /** + * Calls the provided consumer on all player UIs in the list, in order of addition. + * @param consumer the consumer to call with player UIs + */ + public void call(final Consumer consumer) { + //noinspection SimplifyStreamApiCallChains + playerUis.stream().forEachOrdered(consumer); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java new file mode 100644 index 000000000..aa36a6a5a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java @@ -0,0 +1,592 @@ +package org.schabi.newpipe.player.ui; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static org.schabi.newpipe.MainActivity.DEBUG; +import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.animation.AnticipateInterpolator; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.core.math.MathUtils; + +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlayerBinding; +import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.gesture.BasePlayerGestureListener; +import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener; +import org.schabi.newpipe.player.helper.PlayerHelper; + +public final class PopupPlayerUi extends VideoPlayerUi { + private static final String TAG = PopupPlayerUi.class.getSimpleName(); + + /** + * Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using + * NewPipe's popup player. + * + *

+ * This value is hardcoded instead of being get dynamically with the method linked of the + * constant documentation below, because it is not static and popup player layout parameters + * are generated with static methods. + *

+ * + * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE + */ + private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f; + + /*////////////////////////////////////////////////////////////////////////// + // Popup player + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerPopupCloseOverlayBinding closeOverlayBinding; + + private boolean isPopupClosing = false; + + private int screenWidth; + private int screenHeight; + + /*////////////////////////////////////////////////////////////////////////// + // Popup player window manager + //////////////////////////////////////////////////////////////////////////*/ + + public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + + private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup + private final WindowManager windowManager; + + + /*////////////////////////////////////////////////////////////////////////// + // Constructor, setup, destroy + //////////////////////////////////////////////////////////////////////////*/ + //region Constructor, setup, destroy + + public PopupPlayerUi(@NonNull final Player player, + @NonNull final PlayerBinding playerBinding) { + super(player, playerBinding); + windowManager = ContextCompat.getSystemService(context, WindowManager.class); + } + + @Override + public void setupAfterIntent() { + super.setupAfterIntent(); + initPopup(); + initPopupCloseOverlay(); + } + + @Override + BasePlayerGestureListener buildGestureListener() { + return new PopupPlayerGestureListener(this); + } + + @SuppressLint("RtlHardcoded") + private void initPopup() { + if (DEBUG) { + Log.d(TAG, "initPopup() called"); + } + + // Popup is already added to windowManager + if (popupHasParent()) { + return; + } + + updateScreenSize(); + + popupLayoutParams = retrievePopupLayoutParamsFromPrefs(); + binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); + + checkPopupPositionBounds(); + + binding.loadingPanel.setMinimumWidth(popupLayoutParams.width); + binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); + + windowManager.addView(binding.getRoot(), popupLayoutParams); + setupVideoSurfaceIfNeeded(); // now there is a parent, we can setup video surface + + // Popup doesn't have aspectRatio selector, using FIT automatically + setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) { + Log.d(TAG, "initPopupCloseOverlay() called"); + } + + // closeOverlayView is already added to windowManager + if (closeOverlayBinding != null) { + return; + } + + closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context)); + + final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams(); + closeOverlayBinding.closeButton.setVisibility(View.GONE); + windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams); + } + + @Override + protected void setupElementsVisibility() { + binding.fullScreenButton.setVisibility(View.VISIBLE); + binding.screenRotationButton.setVisibility(View.GONE); + binding.resizeTextView.setVisibility(View.GONE); + binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE); + binding.queueButton.setVisibility(View.GONE); + binding.segmentsButton.setVisibility(View.GONE); + binding.moreOptionsButton.setVisibility(View.GONE); + binding.topControls.setOrientation(LinearLayout.HORIZONTAL); + binding.primaryControls.getLayoutParams().width = WRAP_CONTENT; + binding.secondaryControls.setAlpha(1.0f); + binding.secondaryControls.setVisibility(View.VISIBLE); + binding.secondaryControls.setTranslationY(0); + binding.share.setVisibility(View.GONE); + binding.playWithKodi.setVisibility(View.GONE); + binding.openInBrowser.setVisibility(View.GONE); + binding.switchMute.setVisibility(View.GONE); + binding.playerCloseButton.setVisibility(View.GONE); + binding.topControls.bringToFront(); + binding.topControls.setClickable(false); + binding.topControls.setFocusable(false); + binding.bottomControls.bringToFront(); + super.setupElementsVisibility(); + } + + @Override + protected void setupElementsSize(final Resources resources) { + setupElementsSize( + 0, + 0, + resources.getDimensionPixelSize(R.dimen.player_popup_controls_padding), + resources.getDimensionPixelSize(R.dimen.player_popup_buttons_padding) + ); + } + + @Override + public void removeViewFromParent() { + // view was added by windowManager for popup player + windowManager.removeViewImmediate(binding.getRoot()); + } + + @Override + public void destroy() { + super.destroy(); + removePopupFromView(); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast receiver + //////////////////////////////////////////////////////////////////////////*/ + //region Broadcast receiver + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { + updateScreenSize(); + changePopupSize(popupLayoutParams.width); + checkPopupPositionBounds(); + } else if (player.isPlaying() || player.isLoading()) { + if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { + // Use only audio source when screen turns off while popup player is playing + player.useVideoSource(false); + } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { + // Restore video source when screen turns on and user was watching video in popup + player.useVideoSource(true); + } + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Popup position and size + //////////////////////////////////////////////////////////////////////////*/ + //region Popup position and size + + /** + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (screenWidth, screenHeight). + *

+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

+ */ + public void checkPopupPositionBounds() { + if (DEBUG) { + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "screenWidth = [" + screenWidth + "], " + + "screenHeight = [" + screenHeight + "]"); + } + if (popupLayoutParams == null) { + return; + } + + popupLayoutParams.x = MathUtils.clamp(popupLayoutParams.x, 0, screenWidth + - popupLayoutParams.width); + popupLayoutParams.y = MathUtils.clamp(popupLayoutParams.y, 0, screenHeight + - popupLayoutParams.height); + } + + public void updateScreenSize() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final var windowMetrics = windowManager.getCurrentWindowMetrics(); + final var bounds = windowMetrics.getBounds(); + final var windowInsets = windowMetrics.getWindowInsets(); + final var insets = windowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()); + screenWidth = bounds.width() - (insets.left + insets.right); + screenHeight = bounds.height() - (insets.top + insets.bottom); + } else { + final DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + } + if (DEBUG) { + Log.d(TAG, "updateScreenSize() called: screenWidth = [" + + screenWidth + "], screenHeight = [" + screenHeight + "]"); + } + } + + /** + * Changes the size of the popup based on the width. + * @param width the new width, height is calculated with + * {@link PlayerHelper#getMinimumVideoHeight(float)} + */ + public void changePopupSize(final int width) { + if (DEBUG) { + Log.d(TAG, "changePopupSize() called with: width = [" + width + "]"); + } + + if (anyPopupViewIsNull()) { + return; + } + + final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width); + final int actualWidth = Math.min((int) Math.max(width, minimumWidth), screenWidth); + final int actualHeight = (int) getMinimumVideoHeight(width); + if (DEBUG) { + Log.d(TAG, "updatePopupSize() updated values:" + + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); + } + + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); + windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams); + } + + @Override + protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) { + // no need for the end screen thumbnail to be resized on popup player: it's only needed + // for the main player so that it is enlarged correctly inside the fragment + return bitmap.getHeight(); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Popup closing + //////////////////////////////////////////////////////////////////////////*/ + //region Popup closing + + public void closePopup() { + if (DEBUG) { + Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + } + if (isPopupClosing) { + return; + } + isPopupClosing = true; + + player.saveStreamProgressState(); + windowManager.removeView(binding.getRoot()); + + animatePopupOverlayAndFinishService(); + } + + public boolean isPopupClosing() { + return isPopupClosing; + } + + public void removePopupFromView() { + // wrap in try-catch since it could sometimes generate errors randomly + try { + if (popupHasParent()) { + windowManager.removeView(binding.getRoot()); + } + } catch (final IllegalArgumentException e) { + Log.w(TAG, "Failed to remove popup from window manager", e); + } + + try { + final boolean closeOverlayHasParent = closeOverlayBinding != null + && closeOverlayBinding.getRoot().getParent() != null; + if (closeOverlayHasParent) { + windowManager.removeView(closeOverlayBinding.getRoot()); + } + } catch (final IllegalArgumentException e) { + Log.w(TAG, "Failed to remove popup overlay from window manager", e); + } + } + + private void animatePopupOverlayAndFinishService() { + final int targetTranslationY = + (int) (closeOverlayBinding.closeButton.getRootView().getHeight() + - closeOverlayBinding.closeButton.getY()); + + closeOverlayBinding.closeButton.animate().setListener(null).cancel(); + closeOverlayBinding.closeButton.animate() + .setInterpolator(new AnticipateInterpolator()) + .translationY(targetTranslationY) + .setDuration(400) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(final Animator animation) { + end(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + end(); + } + + private void end() { + windowManager.removeView(closeOverlayBinding.getRoot()); + closeOverlayBinding = null; + player.getService().stopService(); + } + }).start(); + } + //endregion + + /*////////////////////////////////////////////////////////////////////////// + // Playback states + //////////////////////////////////////////////////////////////////////////*/ + //region Playback states + + private void changePopupWindowFlags(final int flags) { + if (DEBUG) { + Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]"); + } + + if (!anyPopupViewIsNull()) { + popupLayoutParams.flags = flags; + windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams); + } + } + + @Override + public void onPlaying() { + super.onPlaying(); + changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + } + + @Override + public void onPaused() { + super.onPaused(); + changePopupWindowFlags(IDLE_WINDOW_FLAGS); + } + + @Override + public void onCompleted() { + super.onCompleted(); + changePopupWindowFlags(IDLE_WINDOW_FLAGS); + } + + @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); + } + + @Override + protected void onPlaybackSpeedClicked() { + playbackSpeedPopupMenu.show(); + isSomePopupMenuVisible = true; + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + //region Gestures + + private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft() + + closeOverlayBinding.closeButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop() + + closeOverlayBinding.closeButton.getHeight() / 2; + + final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); + final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); + + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + + Math.pow(closeOverlayButtonY - fingerY, 2)); + } + + private float getClosingRadius() { + final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2; + // 20% wider than the button itself + return buttonRadius * 1.2f; + } + + public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Popup & closing overlay layout params + saving popup position and size + //////////////////////////////////////////////////////////////////////////*/ + //region Popup & closing overlay layout params + saving popup position and size + + /** + * {@code screenWidth} and {@code screenHeight} must have been initialized. + * @return the popup starting layout params + */ + @SuppressLint("RtlHardcoded") + public WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs() { + final SharedPreferences prefs = getPlayer().getPrefs(); + final Context context = getPlayer().getContext(); + + final boolean popupRememberSizeAndPos = prefs.getBoolean( + context.getString(R.string.popup_remember_size_pos_key), true); + final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width); + final float popupWidth = popupRememberSizeAndPos + ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize) + : defaultSize; + final float popupHeight = getMinimumVideoHeight(popupWidth); + + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + (int) popupWidth, (int) popupHeight, + popupLayoutParamType(), + IDLE_WINDOW_FLAGS, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.LEFT | Gravity.TOP; + params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + + final int centerX = (int) (screenWidth / 2f - popupWidth / 2f); + final int centerY = (int) (screenHeight / 2f - popupHeight / 2f); + params.x = popupRememberSizeAndPos + ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX; + params.y = popupRememberSizeAndPos + ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY; + + return params; + } + + public void savePopupPositionAndSizeToPrefs() { + if (getPopupLayoutParams() != null) { + final Context context = getPlayer().getContext(); + getPlayer().getPrefs().edit() + .putFloat(context.getString(R.string.popup_saved_width_key), + popupLayoutParams.width) + .putInt(context.getString(R.string.popup_saved_x_key), + popupLayoutParams.x) + .putInt(context.getString(R.string.popup_saved_y_key), + popupLayoutParams.y) + .apply(); + } + } + + @SuppressLint("RtlHardcoded") + public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() { + final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + + final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, + popupLayoutParamType(), + flags, + PixelFormat.TRANSLUCENT); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Setting maximum opacity allowed for touch events to other apps for Android 12 and + // higher to prevent non interaction when using other apps with the popup player + closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER; + } + + closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + closeOverlayLayoutParams.softInputMode = + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + return closeOverlayLayoutParams; + } + + public static int popupLayoutParamType() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_PHONE + : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + //region Getters + + private boolean popupHasParent() { + return binding != null + && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams + && binding.getRoot().getParent() != null; + } + + private boolean anyPopupViewIsNull() { + return popupLayoutParams == null || windowManager == null + || binding.getRoot().getParent() == null; + } + + public PlayerPopupCloseOverlayBinding getCloseOverlayBinding() { + return closeOverlayBinding; + } + + public WindowManager.LayoutParams getPopupLayoutParams() { + return popupLayoutParams; + } + + public WindowManager getWindowManager() { + return windowManager; + } + + public int getScreenHeight() { + return screenHeight; + } + + public int getScreenWidth() { + return screenWidth; + } + //endregion +} diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java new file mode 100644 index 000000000..1709755f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -0,0 +1,1578 @@ +package org.schabi.newpipe.player.ui; + +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static org.schabi.newpipe.MainActivity.DEBUG; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; +import static org.schabi.newpipe.player.Player.RENDERER_UNAVAILABLE; +import static org.schabi.newpipe.player.Player.STATE_BUFFERING; +import static org.schabi.newpipe.player.Player.STATE_COMPLETED; +import static org.schabi.newpipe.player.Player.STATE_PAUSED; +import static org.schabi.newpipe.player.Player.STATE_PAUSED_SEEK; +import static org.schabi.newpipe.player.Player.STATE_PLAYING; +import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; + +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.graphics.Insets; +import androidx.core.math.MathUtils; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.Tracks; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.CaptionStyleCompat; +import com.google.android.exoplayer2.video.VideoSize; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlayerBinding; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.ktx.AnimationType; +import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.gesture.BasePlayerGestureListener; +import org.schabi.newpipe.player.gesture.DisplayPortion; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; +import org.schabi.newpipe.player.playback.SurfaceHolderCallback; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; +import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public abstract class VideoPlayerUi extends PlayerUi + implements SeekBar.OnSeekBarChangeListener, View.OnClickListener, View.OnLongClickListener, + PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener { + private static final String TAG = VideoPlayerUi.class.getSimpleName(); + + // time constants + public static final long DEFAULT_CONTROLS_DURATION = 300; // 300 millis + public static final long DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final long DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds + public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis + + // other constants (TODO remove playback speeds and use normal menu for popup, too) + private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; + + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + protected PlayerBinding binding; + private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper()); + @Nullable private SurfaceHolderCallback surfaceHolderCallback; + boolean surfaceIsSetup = false; + + + /*////////////////////////////////////////////////////////////////////////// + // Popup menus ("popup" means that they pop up, not that they belong to the popup player) + //////////////////////////////////////////////////////////////////////////*/ + + private static final int POPUP_MENU_ID_QUALITY = 69; + private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; + private static final int POPUP_MENU_ID_CAPTION = 89; + + protected boolean isSomePopupMenuVisible = false; + private PopupMenu qualityPopupMenu; + protected PopupMenu playbackSpeedPopupMenu; + private PopupMenu captionPopupMenu; + + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + + private GestureDetector gestureDetector; + private BasePlayerGestureListener playerGestureListener; + @Nullable private View.OnLayoutChangeListener onLayoutChangeListener = null; + + @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder = + new SeekbarPreviewThumbnailHolder(); + + + /*////////////////////////////////////////////////////////////////////////// + // Constructor, setup, destroy + //////////////////////////////////////////////////////////////////////////*/ + //region Constructor, setup, destroy + + protected VideoPlayerUi(@NonNull final Player player, + @NonNull final PlayerBinding playerBinding) { + super(player); + binding = playerBinding; + setupFromView(); + } + + public void setupFromView() { + initViews(); + initListeners(); + setupPlayerSeekOverlay(); + } + + private void initViews() { + setupSubtitleView(); + + binding.resizeTextView + .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode())); + + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + binding.playbackSeekBar.getProgressDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)); + + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, + R.style.DarkPopupMenu); + + qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView); + playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); + captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView); + + binding.progressBarLoadingPanel.getIndeterminateDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); + + binding.titleTextView.setSelected(true); + binding.channelTextView.setSelected(true); + + // Prevent hiding of bottom sheet via swipe inside queue + binding.itemsList.setNestedScrollingEnabled(false); + } + + abstract BasePlayerGestureListener buildGestureListener(); + + protected void initListeners() { + binding.qualityTextView.setOnClickListener(this); + binding.playbackSpeed.setOnClickListener(this); + + binding.playbackSeekBar.setOnSeekBarChangeListener(this); + binding.captionTextView.setOnClickListener(this); + binding.resizeTextView.setOnClickListener(this); + binding.playbackLiveSync.setOnClickListener(this); + + playerGestureListener = buildGestureListener(); + gestureDetector = new GestureDetector(context, playerGestureListener); + binding.getRoot().setOnTouchListener(playerGestureListener); + + binding.repeatButton.setOnClickListener(v -> onRepeatClicked()); + binding.shuffleButton.setOnClickListener(v -> onShuffleClicked()); + + binding.playPauseButton.setOnClickListener(this); + binding.playPreviousButton.setOnClickListener(this); + binding.playNextButton.setOnClickListener(this); + + binding.moreOptionsButton.setOnClickListener(this); + binding.moreOptionsButton.setOnLongClickListener(this); + binding.share.setOnClickListener(this); + binding.share.setOnLongClickListener(this); + binding.fullScreenButton.setOnClickListener(this); + binding.screenRotationButton.setOnClickListener(this); + binding.playWithKodi.setOnClickListener(this); + binding.openInBrowser.setOnClickListener(this); + binding.playerCloseButton.setOnClickListener(this); + binding.switchMute.setOnClickListener(this); + + ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> { + final Insets cutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()); + if (!cutout.equals(Insets.NONE)) { + view.setPadding(cutout.left, cutout.top, cutout.right, cutout.bottom); + } + return windowInsets; + }); + + // PlaybackControlRoot already consumed window insets but we should pass them to + // player_overlays and fast_seek_overlay too. Without it they will be off-centered. + onLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + binding.playerOverlays.setPadding( + v.getPaddingLeft(), + v.getPaddingTop(), + v.getPaddingRight(), + v.getPaddingBottom()); + + // If we added padding to the fast seek overlay, too, it would not go under the + // system ui. Instead we apply negative margins equal to the window insets of + // the opposite side, so that the view covers all of the player (overflowing on + // some sides) and its center coincides with the center of other controls. + final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams) + binding.fastSeekOverlay.getLayoutParams(); + fastSeekParams.leftMargin = -v.getPaddingRight(); + fastSeekParams.topMargin = -v.getPaddingBottom(); + fastSeekParams.rightMargin = -v.getPaddingLeft(); + fastSeekParams.bottomMargin = -v.getPaddingTop(); + }; + binding.playbackControlRoot.addOnLayoutChangeListener(onLayoutChangeListener); + } + + protected void deinitListeners() { + binding.qualityTextView.setOnClickListener(null); + binding.playbackSpeed.setOnClickListener(null); + binding.playbackSeekBar.setOnSeekBarChangeListener(null); + binding.captionTextView.setOnClickListener(null); + binding.resizeTextView.setOnClickListener(null); + binding.playbackLiveSync.setOnClickListener(null); + + binding.getRoot().setOnTouchListener(null); + playerGestureListener = null; + gestureDetector = null; + + binding.repeatButton.setOnClickListener(null); + binding.shuffleButton.setOnClickListener(null); + + binding.playPauseButton.setOnClickListener(null); + binding.playPreviousButton.setOnClickListener(null); + binding.playNextButton.setOnClickListener(null); + + binding.moreOptionsButton.setOnClickListener(null); + binding.moreOptionsButton.setOnLongClickListener(null); + binding.share.setOnClickListener(null); + binding.share.setOnLongClickListener(null); + binding.fullScreenButton.setOnClickListener(null); + binding.screenRotationButton.setOnClickListener(null); + binding.playWithKodi.setOnClickListener(null); + binding.openInBrowser.setOnClickListener(null); + binding.playerCloseButton.setOnClickListener(null); + binding.switchMute.setOnClickListener(null); + + ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, null); + + binding.playbackControlRoot.removeOnLayoutChangeListener(onLayoutChangeListener); + } + + /** + * Initializes the Fast-For/Backward overlay. + */ + private void setupPlayerSeekOverlay() { + binding.fastSeekOverlay + .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(player) / 1000) + .performListener(new PlayerFastSeekOverlay.PerformListener() { + + @Override + public void onDoubleTap() { + animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION); + } + + @Override + public void onDoubleTapEnd() { + animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION); + } + + @NonNull + @Override + public FastSeekDirection getFastSeekDirection( + @NonNull final DisplayPortion portion + ) { + if (player.exoPlayerIsNull()) { + // Abort seeking + playerGestureListener.endMultiDoubleTap(); + return FastSeekDirection.NONE; + } + if (portion == DisplayPortion.LEFT) { + // Check if it's possible to rewind + // Small puffer to eliminate infinite rewind seeking + if (player.getExoPlayer().getCurrentPosition() < 500L) { + return FastSeekDirection.NONE; + } + return FastSeekDirection.BACKWARD; + } else if (portion == DisplayPortion.RIGHT) { + // Check if it's possible to fast-forward + if (player.getCurrentState() == STATE_COMPLETED + || player.getExoPlayer().getCurrentPosition() + >= player.getExoPlayer().getDuration()) { + return FastSeekDirection.NONE; + } + return FastSeekDirection.FORWARD; + } + /* portion == DisplayPortion.MIDDLE */ + return FastSeekDirection.NONE; + } + + @Override + public void seek(final boolean forward) { + playerGestureListener.keepInDoubleTapMode(); + if (forward) { + player.fastForward(); + } else { + player.fastRewind(); + } + } + }); + playerGestureListener.doubleTapControls(binding.fastSeekOverlay); + } + + public void deinitPlayerSeekOverlay() { + binding.fastSeekOverlay + .seekSecondsSupplier(null) + .performListener(null); + } + + @Override + public void setupAfterIntent() { + super.setupAfterIntent(); + setupElementsVisibility(); + setupElementsSize(context.getResources()); + binding.getRoot().setVisibility(View.VISIBLE); + binding.playPauseButton.requestFocus(); + } + + @Override + public void initPlayer() { + super.initPlayer(); + setupVideoSurfaceIfNeeded(); + } + + @Override + public void initPlayback() { + super.initPlayback(); + + // #6825 - Ensure that the shuffle-button is in the correct state on the UI + setShuffleButton(player.getExoPlayer().getShuffleModeEnabled()); + } + + public abstract void removeViewFromParent(); + + @Override + public void destroyPlayer() { + super.destroyPlayer(); + clearVideoSurface(); + } + + @Override + public void destroy() { + super.destroy(); + binding.endScreen.setImageDrawable(null); + deinitPlayerSeekOverlay(); + deinitListeners(); + } + + protected void setupElementsVisibility() { + setMuteButton(player.isMuted()); + animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); + } + + protected abstract void setupElementsSize(Resources resources); + + protected void setupElementsSize(final int buttonsMinWidth, + final int playerTopPad, + final int controlsPad, + final int buttonsPad) { + binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); + binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); + binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); + binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast receiver + //////////////////////////////////////////////////////////////////////////*/ + //region Broadcast receiver + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { + // When the orientation changes, the screen height might be smaller. If the end screen + // thumbnail is not re-scaled, it can be larger than the current screen height and thus + // enlarging the whole player. This causes the seekbar to be out of the visible area. + updateEndScreenThumbnail(player.getThumbnail()); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail + //////////////////////////////////////////////////////////////////////////*/ + //region Thumbnail + + /** + * Scale the player audio / end screen thumbnail down if necessary. + *

+ * This is necessary when the thumbnail's height is larger than the device's height + * and thus is enlarging the player's height + * causing the bottom playback controls to be out of the visible screen. + *

+ */ + @Override + public void onThumbnailLoaded(@Nullable final Bitmap bitmap) { + super.onThumbnailLoaded(bitmap); + updateEndScreenThumbnail(bitmap); + } + + private void updateEndScreenThumbnail(@Nullable final Bitmap thumbnail) { + if (thumbnail == null) { + // remove end screen thumbnail + binding.endScreen.setImageDrawable(null); + return; + } + + final float endScreenHeight = calculateMaxEndScreenThumbnailHeight(thumbnail); + final Bitmap endScreenBitmap = Bitmap.createScaledBitmap( + thumbnail, + (int) (thumbnail.getWidth() / (thumbnail.getHeight() / endScreenHeight)), + (int) endScreenHeight, + true); + + if (DEBUG) { + Log.d(TAG, "Thumbnail - onThumbnailLoaded() called with: " + + "currentThumbnail = [" + thumbnail + "], " + + thumbnail.getWidth() + "x" + thumbnail.getHeight() + + ", scaled end screen height = " + endScreenHeight + + ", scaled end screen width = " + endScreenBitmap.getWidth()); + } + + binding.endScreen.setImageBitmap(endScreenBitmap); + } + + protected abstract float calculateMaxEndScreenThumbnailHeight(@NonNull Bitmap bitmap); + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Progress loop and updates + //////////////////////////////////////////////////////////////////////////*/ + //region Progress loop and updates + + @Override + public void onUpdateProgress(final int currentProgress, + final int duration, + final int bufferPercent) { + + if (duration != binding.playbackSeekBar.getMax()) { + setVideoDurationToControls(duration); + } + if (player.getCurrentState() != STATE_PAUSED) { + updatePlayBackElementsCurrentDuration(currentProgress); + } + if (player.isLoading() || bufferPercent > 90) { + binding.playbackSeekBar.setSecondaryProgress( + (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100))); + } + if (DEBUG && bufferPercent % 20 == 0) { //Limit log + Log.d(TAG, "notifyProgressUpdateToListeners() called with: " + + "isVisible = " + isControlsVisible() + ", " + + "currentProgress = [" + currentProgress + "], " + + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + } + binding.playbackLiveSync.setClickable(!player.isLiveEdge()); + } + + /** + * Sets the current duration into the corresponding elements. + * @param currentProgress the current progress, in milliseconds + */ + private void updatePlayBackElementsCurrentDuration(final int currentProgress) { + // Don't set seekbar progress while user is seeking + if (player.getCurrentState() != STATE_PAUSED_SEEK) { + binding.playbackSeekBar.setProgress(currentProgress); + } + binding.playbackCurrentTime.setText(getTimeString(currentProgress)); + } + + /** + * Sets the video duration time into all control components (e.g. seekbar). + * @param duration the video duration, in milliseconds + */ + private void setVideoDurationToControls(final int duration) { + binding.playbackEndTime.setText(getTimeString(duration)); + + binding.playbackSeekBar.setMax(duration); + // This is important for Android TVs otherwise it would apply the default from + // setMax/Min methods which is (max - min) / 20 + binding.playbackSeekBar.setKeyProgressIncrement( + PlayerHelper.retrieveSeekDurationFromPreferences(player)); + } + + @Override // seekbar listener + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { + // Currently we don't need method execution when fromUser is false + if (!fromUser) { + return; + } + if (DEBUG) { + Log.d(TAG, "onProgressChanged() called with: " + + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); + } + + binding.currentDisplaySeek.setText(getTimeString(progress)); + + // Seekbar Preview Thumbnail + SeekbarPreviewThumbnailHelper + .tryResizeAndSetSeekbarPreviewThumbnail( + player.getContext(), + seekbarPreviewThumbnailHolder.getBitmapAt(progress), + binding.currentSeekbarPreviewThumbnail, + binding.subtitleView::getWidth); + + adjustSeekbarPreviewContainer(); + } + + + private void adjustSeekbarPreviewContainer() { + try { + // Should only be required when an error occurred before + // and the layout was positioned in the center + binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY); + + // Calculate the current left position of seekbar progress in px + // More info: https://stackoverflow.com/q/20493577 + final int currentSeekbarLeft = + binding.playbackSeekBar.getLeft() + + binding.playbackSeekBar.getPaddingLeft() + + binding.playbackSeekBar.getThumb().getBounds().left; + + // Calculate the (unchecked) left position of the container + final int uncheckedContainerLeft = + currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2); + + // Fix the position so it's within the boundaries + final int checkedContainerLeft = MathUtils.clamp(uncheckedContainerLeft, + 0, binding.playbackWindowRoot.getWidth() + - binding.seekbarPreviewContainer.getWidth()); + + // See also: https://stackoverflow.com/a/23249734 + final LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams( + binding.seekbarPreviewContainer.getLayoutParams()); + params.setMarginStart(checkedContainerLeft); + binding.seekbarPreviewContainer.setLayoutParams(params); + } catch (final Exception ex) { + Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex); + // Fallback - position in the middle + binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER); + } + } + + @Override // seekbar listener + public void onStartTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + if (player.getCurrentState() != STATE_PAUSED_SEEK) { + player.changeState(STATE_PAUSED_SEEK); + } + + player.saveWasPlaying(); + if (player.isPlaying()) { + player.getExoPlayer().pause(); + } + + showControls(0); + animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION, + AnimationType.SCALE_AND_ALPHA); + animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION, + AnimationType.SCALE_AND_ALPHA); + } + + @Override // seekbar listener + public void onStopTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + + player.seekTo(seekBar.getProgress()); + if (player.wasPlaying() || player.getExoPlayer().getDuration() == seekBar.getProgress()) { + player.getExoPlayer().play(); + } + + binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); + animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); + animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA); + + if (player.getCurrentState() == STATE_PAUSED_SEEK) { + player.changeState(STATE_BUFFERING); + } + if (!player.isProgressLoopRunning()) { + player.startProgressLoop(); + } + if (player.wasPlaying()) { + showControlsThenHide(); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Controls showing / hiding + //////////////////////////////////////////////////////////////////////////*/ + //region Controls showing / hiding + + public boolean isControlsVisible() { + return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE; + } + + public void showControlsThenHide() { + if (DEBUG) { + Log.d(TAG, "showControlsThenHide() called"); + } + + showOrHideButtons(); + showSystemUIPartially(); + + final long hideTime = binding.playbackControlRoot.isInTouchMode() + ? DEFAULT_CONTROLS_HIDE_TIME + : DPAD_CONTROLS_HIDE_TIME; + + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, + AnimationType.ALPHA, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); + } + + public void showControls(final long duration) { + if (DEBUG) { + Log.d(TAG, "showControls() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, duration); + animate(binding.playbackControlRoot, true, duration); + } + + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: duration = [" + duration + + "], delay = [" + delay + "]"); + } + + showOrHideButtons(); + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(() -> { + showHideShadow(false, duration); + animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA, + 0, this::hideSystemUIIfNeeded); + }, delay); + } + + public void showHideShadow(final boolean show, final long duration) { + animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null); + animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null); + animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null); + } + + protected void showOrHideButtons() { + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue == null) { + return; + } + + final boolean showPrev = playQueue.getIndex() != 0; + final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); + + binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); + binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); + binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); + binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f); + } + + protected void showSystemUIPartially() { + // system UI is really changed only by MainPlayerUi, so overridden there + } + + protected void hideSystemUIIfNeeded() { + // system UI is really changed only by MainPlayerUi, so overridden there + } + + protected boolean isAnyListViewOpen() { + // only MainPlayerUi has list views for the queue and for segments, so overridden there + return false; + } + + public boolean isFullscreen() { + // only MainPlayerUi can be in fullscreen, so overridden there + return false; + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Playback states + //////////////////////////////////////////////////////////////////////////*/ + //region Playback states + + @Override + public void onPrepared() { + super.onPrepared(); + setVideoDurationToControls((int) player.getExoPlayer().getDuration()); + binding.playbackSpeed.setText(formatSpeed(player.getPlaybackSpeed())); + } + + @Override + public void onBlocked() { + super.onBlocked(); + + // if we are e.g. switching players, hide controls + hideControls(DEFAULT_CONTROLS_DURATION, 0); + + binding.playbackSeekBar.setEnabled(false); + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + + binding.loadingPanel.setBackgroundColor(Color.BLACK); + animate(binding.loadingPanel, true, 0); + animate(binding.surfaceForeground, true, 100); + + binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow); + animatePlayButtons(false, 100); + binding.getRoot().setKeepScreenOn(false); + } + + @Override + public void onPlaying() { + super.onPlaying(); + + updateStreamRelatedViews(); + + binding.playbackSeekBar.setEnabled(true); + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + + binding.loadingPanel.setVisibility(View.GONE); + + animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); + + animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_pause); + animatePlayButtons(true, 200); + if (!isAnyListViewOpen()) { + binding.playPauseButton.requestFocus(); + } + }); + + binding.getRoot().setKeepScreenOn(true); + } + + @Override + public void onBuffering() { + super.onBuffering(); + binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT); + binding.loadingPanel.setVisibility(View.VISIBLE); + binding.getRoot().setKeepScreenOn(true); + } + + @Override + public void onPaused() { + super.onPaused(); + + // Don't let UI elements popup during double tap seeking. This state is entered sometimes + // during seeking/loading. This if-else check ensures that the controls aren't popping up. + if (!playerGestureListener.isDoubleTapping()) { + showControls(400); + binding.loadingPanel.setVisibility(View.GONE); + + animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow); + animatePlayButtons(true, 200); + if (!isAnyListViewOpen()) { + binding.playPauseButton.requestFocus(); + } + }); + } + + binding.getRoot().setKeepScreenOn(false); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + animatePlayButtons(false, 100); + binding.getRoot().setKeepScreenOn(true); + } + + @Override + public void onCompleted() { + super.onCompleted(); + + animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_replay); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + }); + + binding.getRoot().setKeepScreenOn(false); + + // When a (short) video ends the elements have to display the correct values - see #6180 + updatePlayBackElementsCurrentDuration(binding.playbackSeekBar.getMax()); + + showControls(500); + animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); + binding.loadingPanel.setVisibility(View.GONE); + animate(binding.surfaceForeground, true, 100); + } + + private void animatePlayButtons(final boolean show, final long duration) { + animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA); + + @Nullable final PlayQueue playQueue = player.getPlayQueue(); + if (playQueue == null) { + return; + } + + if (!show || playQueue.getIndex() > 0) { + animate( + binding.playPreviousButton, + show, + duration, + AnimationType.SCALE_AND_ALPHA); + } + if (!show || playQueue.getIndex() + 1 < playQueue.getStreams().size()) { + animate( + binding.playNextButton, + show, + duration, + AnimationType.SCALE_AND_ALPHA); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Repeat, shuffle, mute + //////////////////////////////////////////////////////////////////////////*/ + //region Repeat, shuffle, mute + + public void onRepeatClicked() { + if (DEBUG) { + Log.d(TAG, "onRepeatClicked() called"); + } + player.cycleNextRepeatMode(); + } + + public void onShuffleClicked() { + if (DEBUG) { + Log.d(TAG, "onShuffleClicked() called"); + } + player.toggleShuffleModeEnabled(); + } + + @Override + public void onRepeatModeChanged(@RepeatMode final int repeatMode) { + super.onRepeatModeChanged(repeatMode); + + if (repeatMode == REPEAT_MODE_ALL) { + binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_all); + } else if (repeatMode == REPEAT_MODE_ONE) { + binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_one); + } else /* repeatMode == REPEAT_MODE_OFF */ { + binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_off); + } + } + + @Override + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled); + setShuffleButton(shuffleModeEnabled); + } + + @Override + public void onMuteUnmuteChanged(final boolean isMuted) { + super.onMuteUnmuteChanged(isMuted); + setMuteButton(isMuted); + } + + private void setMuteButton(final boolean isMuted) { + binding.switchMute.setImageDrawable(AppCompatResources.getDrawable(context, isMuted + ? R.drawable.ic_volume_off : R.drawable.ic_volume_up)); + } + + private void setShuffleButton(final boolean shuffled) { + binding.shuffleButton.setImageAlpha(shuffled ? 255 : 77); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Other player listeners + //////////////////////////////////////////////////////////////////////////*/ + //region Other player listeners + + @Override + public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { + super.onPlaybackParametersChanged(playbackParameters); + binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed)); + } + + @Override + public void onRenderedFirstFrame() { + super.onRenderedFirstFrame(); + //TODO check if this causes black screen when switching to fullscreen + animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Metadata & stream related views + //////////////////////////////////////////////////////////////////////////*/ + //region Metadata & stream related views + + @Override + public void onMetadataChanged(@NonNull final StreamInfo info) { + super.onMetadataChanged(info); + + updateStreamRelatedViews(); + + binding.titleTextView.setText(info.getName()); + binding.channelTextView.setText(info.getUploaderName()); + + this.seekbarPreviewThumbnailHolder.resetFrom(player.getContext(), info.getPreviewFrames()); + } + + private void updateStreamRelatedViews() { + //noinspection SimplifyOptionalCallChains + if (!player.getCurrentStreamInfo().isPresent()) { + return; + } + final StreamInfo info = player.getCurrentStreamInfo().get(); + + binding.qualityTextView.setVisibility(View.GONE); + binding.playbackSpeed.setVisibility(View.GONE); + + binding.playbackEndTime.setVisibility(View.GONE); + binding.playbackLiveSync.setVisibility(View.GONE); + + switch (info.getStreamType()) { + case AUDIO_STREAM: + case POST_LIVE_AUDIO_STREAM: + binding.surfaceView.setVisibility(View.GONE); + binding.endScreen.setVisibility(View.VISIBLE); + binding.playbackEndTime.setVisibility(View.VISIBLE); + break; + + case AUDIO_LIVE_STREAM: + binding.surfaceView.setVisibility(View.GONE); + binding.endScreen.setVisibility(View.VISIBLE); + binding.playbackLiveSync.setVisibility(View.VISIBLE); + break; + + case LIVE_STREAM: + binding.surfaceView.setVisibility(View.VISIBLE); + binding.endScreen.setVisibility(View.GONE); + binding.playbackLiveSync.setVisibility(View.VISIBLE); + break; + + case VIDEO_STREAM: + case POST_LIVE_STREAM: + //noinspection SimplifyOptionalCallChains + if (player.getCurrentMetadata() != null + && !player.getCurrentMetadata().getMaybeQuality().isPresent() + || (info.getVideoStreams().isEmpty() + && info.getVideoOnlyStreams().isEmpty())) { + break; + } + + buildQualityMenu(); + + binding.qualityTextView.setVisibility(View.VISIBLE); + binding.surfaceView.setVisibility(View.VISIBLE); + // fallthrough + default: + binding.endScreen.setVisibility(View.GONE); + binding.playbackEndTime.setVisibility(View.VISIBLE); + break; + } + + buildPlaybackSpeedMenu(); + binding.playbackSpeed.setVisibility(View.VISIBLE); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Popup menus ("popup" means that they pop up, not that they belong to the popup player) + //////////////////////////////////////////////////////////////////////////*/ + //region Popup menus ("popup" means that they pop up, not that they belong to the popup player) + + private void buildQualityMenu() { + if (qualityPopupMenu == null) { + return; + } + qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY); + + final List availableStreams = Optional.ofNullable(player.getCurrentMetadata()) + .flatMap(MediaItemTag::getMaybeQuality) + .map(MediaItemTag.Quality::getSortedVideoStreams) + .orElse(null); + if (availableStreams == null) { + return; + } + + for (int i = 0; i < availableStreams.size(); i++) { + final VideoStream videoStream = availableStreams.get(i); + qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat + .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution()); + } + final VideoStream selectedVideoStream = player.getSelectedVideoStream(); + if (selectedVideoStream != null) { + binding.qualityTextView.setText(selectedVideoStream.getResolution()); + } + qualityPopupMenu.setOnMenuItemClickListener(this); + qualityPopupMenu.setOnDismissListener(this); + } + + private void buildPlaybackSpeedMenu() { + if (playbackSpeedPopupMenu == null) { + return; + } + playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED); + + for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { + playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE, + formatSpeed(PLAYBACK_SPEEDS[i])); + } + binding.playbackSpeed.setText(formatSpeed(player.getPlaybackSpeed())); + playbackSpeedPopupMenu.setOnMenuItemClickListener(this); + playbackSpeedPopupMenu.setOnDismissListener(this); + } + + private void buildCaptionMenu(@NonNull final List availableLanguages) { + if (captionPopupMenu == null) { + return; + } + captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION); + + captionPopupMenu.setOnDismissListener(this); + + // Add option for turning off caption + final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, + 0, Menu.NONE, R.string.caption_none); + captionOffItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = player.getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + player.getTrackSelector().setParameters(player.getTrackSelector() + .buildUponParameters().setRendererDisabled(textRendererIndex, true)); + } + player.getPrefs().edit() + .remove(context.getString(R.string.caption_user_set_key)).apply(); + return true; + }); + + // Add all available captions + for (int i = 0; i < availableLanguages.size(); i++) { + final String captionLanguage = availableLanguages.get(i); + final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, + i + 1, Menu.NONE, captionLanguage); + captionItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = player.getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + // DefaultTrackSelector will select for text tracks in the following order. + // When multiple tracks share the same rank, a random track will be chosen. + // 1. ANY track exactly matching preferred language name + // 2. ANY track exactly matching preferred language stem + // 3. ROLE_FLAG_CAPTION track matching preferred language stem + // 4. ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND track matching preferred language stem + // This means if a caption track of preferred language is not available, + // then an auto-generated track of that language will be chosen automatically. + player.getTrackSelector().setParameters(player.getTrackSelector() + .buildUponParameters() + .setPreferredTextLanguages(captionLanguage, + PlayerHelper.captionLanguageStemOf(captionLanguage)) + .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) + .setRendererDisabled(textRendererIndex, false)); + player.getPrefs().edit().putString(context.getString( + R.string.caption_user_set_key), captionLanguage).apply(); + } + return true; + }); + } + captionPopupMenu.setOnDismissListener(this); + + // apply caption language from previous user preference + final int textRendererIndex = player.getCaptionRendererIndex(); + if (textRendererIndex == RENDERER_UNAVAILABLE) { + return; + } + + // If user prefers to show no caption, then disable the renderer. + // Otherwise, DefaultTrackSelector may automatically find an available caption + // and display that. + final String userPreferredLanguage = + player.getPrefs().getString(context.getString(R.string.caption_user_set_key), null); + if (userPreferredLanguage == null) { + player.getTrackSelector().setParameters(player.getTrackSelector().buildUponParameters() + .setRendererDisabled(textRendererIndex, true)); + return; + } + + // Only set preferred language if it does not match the user preference, + // otherwise there might be an infinite cycle at onTextTracksChanged. + final List selectedPreferredLanguages = + player.getTrackSelector().getParameters().preferredTextLanguages; + if (!selectedPreferredLanguages.contains(userPreferredLanguage)) { + player.getTrackSelector().setParameters(player.getTrackSelector().buildUponParameters() + .setPreferredTextLanguages(userPreferredLanguage, + PlayerHelper.captionLanguageStemOf(userPreferredLanguage)) + .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) + .setRendererDisabled(textRendererIndex, false)); + } + } + + protected abstract void onPlaybackSpeedClicked(); + + private void onQualityClicked() { + qualityPopupMenu.show(); + isSomePopupMenuVisible = true; + + final VideoStream videoStream = player.getSelectedVideoStream(); + if (videoStream != null) { + //noinspection SetTextI18n + binding.qualityTextView.setText(MediaFormat.getNameById(videoStream.getFormatId()) + + " " + videoStream.getResolution()); + } + + player.saveWasPlaying(); + } + + /** + * Called when an item of the quality selector or the playback speed selector is selected. + */ + @Override + public boolean onMenuItemClick(@NonNull final MenuItem menuItem) { + if (DEBUG) { + Log.d(TAG, "onMenuItemClick() called with: " + + "menuItem = [" + menuItem + "], " + + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); + } + + if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { + final int menuItemIndex = menuItem.getItemId(); + @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); + //noinspection SimplifyOptionalCallChains + if (currentMetadata == null || !currentMetadata.getMaybeQuality().isPresent()) { + return true; + } + + final MediaItemTag.Quality quality = currentMetadata.getMaybeQuality().get(); + final List availableStreams = quality.getSortedVideoStreams(); + final int selectedStreamIndex = quality.getSelectedVideoStreamIndex(); + if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) { + return true; + } + + player.saveStreamProgressState(); //TODO added, check if good + final String newResolution = availableStreams.get(menuItemIndex).getResolution(); + player.setRecovery(); + player.setPlaybackQuality(newResolution); + player.reloadPlayQueueManager(); + + binding.qualityTextView.setText(menuItem.getTitle()); + return true; + } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { + final int speedIndex = menuItem.getItemId(); + final float speed = PLAYBACK_SPEEDS[speedIndex]; + + player.setPlaybackSpeed(speed); + binding.playbackSpeed.setText(formatSpeed(speed)); + } + + return false; + } + + /** + * Called when some popup menu is dismissed. + */ + @Override + public void onDismiss(@Nullable final PopupMenu menu) { + if (DEBUG) { + Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + } + isSomePopupMenuVisible = false; //TODO check if this works + final VideoStream selectedVideoStream = player.getSelectedVideoStream(); + if (selectedVideoStream != null) { + binding.qualityTextView.setText(selectedVideoStream.getResolution()); + } + if (player.isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUIIfNeeded(); + } + } + + private void onCaptionClicked() { + if (DEBUG) { + Log.d(TAG, "onCaptionClicked() called"); + } + captionPopupMenu.show(); + isSomePopupMenuVisible = true; + } + + public boolean isSomePopupMenuVisible() { + return isSomePopupMenuVisible; + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Captions (text tracks) + //////////////////////////////////////////////////////////////////////////*/ + //region Captions (text tracks) + + @Override + public void onTextTracksChanged(@NonNull final Tracks currentTracks) { + super.onTextTracksChanged(currentTracks); + + final boolean trackTypeTextSupported = !currentTracks.containsType(C.TRACK_TYPE_TEXT) + || currentTracks.isTypeSupported(C.TRACK_TYPE_TEXT, false); + if (getPlayer().getTrackSelector().getCurrentMappedTrackInfo() == null + || !trackTypeTextSupported) { + binding.captionTextView.setVisibility(View.GONE); + return; + } + + // Extract all loaded languages + final List textTracks = currentTracks + .getGroups() + .stream() + .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getType()) + .collect(Collectors.toList()); + final List availableLanguages = textTracks.stream() + .map(Tracks.Group::getMediaTrackGroup) + .filter(textTrack -> textTrack.length > 0) + .map(textTrack -> textTrack.getFormat(0).language) + .collect(Collectors.toList()); + + // Find selected text track + final Optional selectedTracks = textTracks.stream() + .filter(Tracks.Group::isSelected) + .filter(info -> info.getMediaTrackGroup().length >= 1) + .map(info -> info.getMediaTrackGroup().getFormat(0)) + .findFirst(); + + // Build UI + buildCaptionMenu(availableLanguages); + //noinspection SimplifyOptionalCallChains + if (player.getTrackSelector().getParameters().getRendererDisabled( + player.getCaptionRendererIndex()) || !selectedTracks.isPresent()) { + binding.captionTextView.setText(R.string.caption_none); + } else { + binding.captionTextView.setText(selectedTracks.get().language); + } + binding.captionTextView.setVisibility( + availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); + } + + @Override + public void onCues(@NonNull final List cues) { + super.onCues(cues); + binding.subtitleView.setCues(cues); + } + + private void setupSubtitleView() { + setupSubtitleView(PlayerHelper.getCaptionScale(context)); + final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); + binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT); + binding.subtitleView.setStyle(captionStyle); + } + + protected abstract void setupSubtitleView(float captionScale); + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Click listeners + //////////////////////////////////////////////////////////////////////////*/ + //region Click listeners + + @Override + public void onClick(final View v) { + if (DEBUG) { + Log.d(TAG, "onClick() called with: v = [" + v + "]"); + } + if (v.getId() == binding.resizeTextView.getId()) { + onResizeClicked(); + } else if (v.getId() == binding.captionTextView.getId()) { + onCaptionClicked(); + } else if (v.getId() == binding.playbackLiveSync.getId()) { + player.seekToDefault(); + } else if (v.getId() == binding.playPauseButton.getId()) { + player.playPause(); + } else if (v.getId() == binding.playPreviousButton.getId()) { + player.playPrevious(); + } else if (v.getId() == binding.playNextButton.getId()) { + player.playNext(); + } else if (v.getId() == binding.moreOptionsButton.getId()) { + onMoreOptionsClicked(); + } else if (v.getId() == binding.share.getId()) { + final PlayQueueItem currentItem = player.getCurrentItem(); + if (currentItem != null) { + ShareUtils.shareText(context, currentItem.getTitle(), + player.getVideoUrlAtCurrentTime(), currentItem.getThumbnailUrl()); + } + } else if (v.getId() == binding.playWithKodi.getId()) { + onPlayWithKodiClicked(); + } else if (v.getId() == binding.openInBrowser.getId()) { + onOpenInBrowserClicked(); + } else if (v.getId() == binding.fullScreenButton.getId()) { + player.setRecovery(); + NavigationHelper.playOnMainPlayer(context, player.getPlayQueue(), true); + return; + } else if (v.getId() == binding.switchMute.getId()) { + player.toggleMute(); + } else if (v.getId() == binding.playerCloseButton.getId()) { + // set package to this app's package to prevent the intent from being seen outside + context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER) + .setPackage(App.PACKAGE_NAME)); + } else if (v.getId() == binding.playbackSpeed.getId()) { + onPlaybackSpeedClicked(); + } else if (v.getId() == binding.qualityTextView.getId()) { + onQualityClicked(); + } + + manageControlsAfterOnClick(v); + } + + /** + * Manages the controls after a click occurred on the player UI. + * @param v – The view that was clicked + */ + public void manageControlsAfterOnClick(@NonNull final View v) { + if (player.getCurrentState() == STATE_COMPLETED) { + return; + } + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, + AnimationType.ALPHA, 0, () -> { + if (player.getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible) { + if (v.getId() == binding.playPauseButton.getId() + // Hide controls in fullscreen immediately + || (v.getId() == binding.screenRotationButton.getId() + && isFullscreen())) { + hideControls(0, 0); + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + }); + } + + @Override + public boolean onLongClick(final View v) { + if (v.getId() == binding.share.getId()) { + ShareUtils.copyToClipboard(context, player.getVideoUrlAtCurrentTime()); + } + return true; + } + + public boolean onKeyDown(final int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (DeviceUtils.isTv(context) && isControlsVisible()) { + hideControls(0, 0); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) + || isAnyListViewOpen()) { + // do not interfere with focus in playlist and play queue etc. + break; + } + + if (player.getCurrentState() == org.schabi.newpipe.player.Player.STATE_BLOCKED) { + return true; + } + + if (isControlsVisible()) { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } else { + binding.playPauseButton.requestFocus(); + showControlsThenHide(); + showSystemUIPartially(); + return true; + } + break; + default: + break; // ignore other keys + } + + return false; + } + + private void onMoreOptionsClicked() { + if (DEBUG) { + Log.d(TAG, "onMoreOptionsClicked() called"); + } + + final boolean isMoreControlsVisible = + binding.secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION, + AnimationType.SLIDE_AND_ALPHA, 0, () -> { + // Fix for a ripple effect on background drawable. + // When view returns from GONE state it takes more milliseconds than returning + // from INVISIBLE state. And the delay makes ripple background end to fast + if (isMoreControlsVisible) { + binding.secondaryControls.setVisibility(View.INVISIBLE); + } + }); + showControls(DEFAULT_CONTROLS_DURATION); + } + + private void onPlayWithKodiClicked() { + if (player.getCurrentMetadata() != null) { + player.pause(); + try { + NavigationHelper.playWithKore(context, Uri.parse(player.getVideoUrl())); + } catch (final Exception e) { + if (DEBUG) { + Log.i(TAG, "Failed to start kore", e); + } + KoreUtils.showInstallKoreDialog(player.getContext()); + } + } + } + + private void onOpenInBrowserClicked() { + player.getCurrentStreamInfo().ifPresent(streamInfo -> + ShareUtils.openUrlInBrowser(player.getContext(), streamInfo.getOriginalUrl())); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Video size + //////////////////////////////////////////////////////////////////////////*/ + //region Video size + + protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { + binding.surfaceView.setResizeMode(resizeMode); + binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode)); + } + + void onResizeClicked() { + setResizeMode(nextResizeModeAndSaveToPrefs(player, binding.surfaceView.getResizeMode())); + } + + @Override + public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { + super.onVideoSizeChanged(videoSize); + binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // SurfaceHolderCallback helpers + //////////////////////////////////////////////////////////////////////////*/ + //region SurfaceHolderCallback helpers + + /** + * Connects the video surface to the exo player. This can be called anytime without the risk for + * issues to occur, since the player will run just fine when no surface is connected. Therefore + * the video surface will be setup only when all of these conditions are true: it is not already + * setup (this just prevents wasting resources to setup the surface again), there is an exo + * player, the root view is attached to a parent and the surface view is valid/unreleased (the + * latter two conditions prevent "The surface has been released" errors). So this function can + * be called many times and even while the UI is in unready states. + */ + public void setupVideoSurfaceIfNeeded() { + if (!surfaceIsSetup && player.getExoPlayer() != null + && binding.getRoot().getParent() != null) { + // make sure there is nothing left over from previous calls + clearVideoSurface(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 + surfaceHolderCallback = new SurfaceHolderCallback(context, player.getExoPlayer()); + binding.surfaceView.getHolder().addCallback(surfaceHolderCallback); + + // ensure player is using an unreleased surface, which the surfaceView might not be + // when starting playback on background or during player switching + if (binding.surfaceView.getHolder().getSurface().isValid()) { + // initially set the surface manually otherwise + // onRenderedFirstFrame() will not be called + player.getExoPlayer().setVideoSurfaceHolder(binding.surfaceView.getHolder()); + } + } else { + player.getExoPlayer().setVideoSurfaceView(binding.surfaceView); + } + + surfaceIsSetup = true; + } + } + + private void clearVideoSurface() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M // >=API23 + && surfaceHolderCallback != null) { + binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); + surfaceHolderCallback.release(); + surfaceHolderCallback = null; + } + Optional.ofNullable(player.getExoPlayer()).ifPresent(ExoPlayer::clearVideoSurface); + surfaceIsSetup = false; + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + //region Getters + + public PlayerBinding getBinding() { + return binding; + } + + public GestureDetector getGestureDetector() { + return gestureDetector; + } + //endregion +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index 70ac5cdcc..550d64d06 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -74,7 +74,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); defaultPreferences.edit().putString(themeKey, newValue.toString()).apply(); - ThemeHelper.setDayNightMode(getContext(), newValue.toString()); + ThemeHelper.setDayNightMode(requireContext(), newValue.toString()); if (!newValue.equals(beginningThemeKey) && getActivity() != null) { // if it's not the current theme diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 47458ad3f..37f83057b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -43,8 +43,8 @@ import java.util.Objects; public class ContentSettingsFragment extends BasePreferenceFragment { private static final String ZIP_MIME_TYPE = "application/zip"; - private final SimpleDateFormat exportDateFormat - = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + private final SimpleDateFormat exportDateFormat = + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); private ContentSettingsManager manager; @@ -160,8 +160,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { // will be saved only on success final Uri lastExportDataUri = result.getData().getData(); - final StoredFileHelper file - = new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE); + final StoredFileHelper file = + new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE); exportDatabase(file, lastExportDataUri); } @@ -173,8 +173,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { // will be saved only on success final Uri lastImportDataUri = result.getData().getData(); - final StoredFileHelper file - = new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE); + final StoredFileHelper file = + new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE); new AlertDialog.Builder(requireActivity()) .setMessage(R.string.override_current_data) diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt index 3ac275695..8adf6a649 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt @@ -5,9 +5,6 @@ import android.util.Log import org.schabi.newpipe.streams.io.SharpOutputStream import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.ZipHelper -import java.io.BufferedOutputStream -import java.io.FileInputStream -import java.io.FileOutputStream import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -25,12 +22,12 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) { @Throws(Exception::class) fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) { file.create() - ZipOutputStream(BufferedOutputStream(SharpOutputStream(file.stream))) + ZipOutputStream(SharpOutputStream(file.stream).buffered()) .use { outZip -> ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db") try { - ObjectOutputStream(FileOutputStream(fileLocator.settings)).use { output -> + ObjectOutputStream(fileLocator.settings.outputStream()).use { output -> output.writeObject(preferences.all) output.flush() } @@ -74,7 +71,7 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) { try { val preferenceEditor = preferences.edit() - ObjectInputStream(FileInputStream(fileLocator.settings)).use { input -> + ObjectInputStream(fileLocator.settings.inputStream()).use { input -> preferenceEditor.clear() @Suppress("UNCHECKED_CAST") val entries = input.readObject() as Map diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index dd9f5fb1f..0f4c9765e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -9,8 +9,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.local.feed.notifications.NotificationWorker; +import org.schabi.newpipe.util.PicassoHelper; import java.util.Optional; @@ -21,20 +21,20 @@ public class DebugSettingsFragment extends BasePreferenceFragment { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResourceRegistry(); - final Preference allowHeapDumpingPreference - = findPreference(getString(R.string.allow_heap_dumping_key)); - final Preference showMemoryLeaksPreference - = findPreference(getString(R.string.show_memory_leaks_key)); - final Preference showImageIndicatorsPreference - = findPreference(getString(R.string.show_image_indicators_key)); - final Preference checkNewStreamsPreference - = findPreference(getString(R.string.check_new_streams_key)); - final Preference crashTheAppPreference - = findPreference(getString(R.string.crash_the_app_key)); - final Preference showErrorSnackbarPreference - = findPreference(getString(R.string.show_error_snackbar_key)); - final Preference createErrorNotificationPreference - = findPreference(getString(R.string.create_error_notification_key)); + final Preference allowHeapDumpingPreference = + findPreference(getString(R.string.allow_heap_dumping_key)); + final Preference showMemoryLeaksPreference = + findPreference(getString(R.string.show_memory_leaks_key)); + final Preference showImageIndicatorsPreference = + findPreference(getString(R.string.show_image_indicators_key)); + final Preference checkNewStreamsPreference = + findPreference(getString(R.string.check_new_streams_key)); + final Preference crashTheAppPreference = + findPreference(getString(R.string.crash_the_app_key)); + final Preference showErrorSnackbarPreference = + findPreference(getString(R.string.show_error_snackbar_key)); + final Preference createErrorNotificationPreference = + findPreference(getString(R.string.create_error_notification_key)); assert allowHeapDumpingPreference != null; assert showMemoryLeaksPreference != null; diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index ec98b865e..5a4300cdd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.settings; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.app.Activity; import android.content.ContentResolver; import android.content.Context; @@ -32,8 +34,6 @@ import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - public class DownloadSettingsFragment extends BasePreferenceFragment { public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true; private String downloadPathVideoPreference; @@ -66,16 +66,10 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { prefStorageAsk = findPreference(downloadStorageAsk); final SwitchPreferenceCompat prefUseSaf = findPreference(storageUseSafPreference); - prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { prefUseSaf.setEnabled(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29); - } else { - prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_19); - } + prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29); prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary_no_saf_notice); } @@ -253,8 +247,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { forgetSAFTree(context, defaultPreferences.getString(key, "")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && !FilePickerActivityHelper.isOwnFileUri(context, uri)) { + if (!FilePickerActivityHelper.isOwnFileUri(context, uri)) { // steps to acquire the selected path: // 1. acquire permissions on the new save path // 2. save the new path, if step(2) was successful @@ -262,8 +255,8 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { context.grantUriPermission(context.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); - final StoredDirectoryHelper mainStorage - = new StoredDirectoryHelper(context, uri, null); + final StoredDirectoryHelper mainStorage = + new StoredDirectoryHelper(context, uri, null); Log.i(TAG, "Acquiring tree success from " + uri.toString()); if (!mainStorage.canWrite()) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 1e1d08856..16df646f9 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -116,7 +116,7 @@ public final class NewPipeSettings { public static boolean useStorageAccessFramework(final Context context) { // There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with a // remote (see #6455). - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || DeviceUtils.isFireTv()) { + if (DeviceUtils.isFireTv()) { return false; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return true; diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt index 6bea8b69e..11eb4fa33 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationSettingsFragment.kt @@ -1,19 +1,9 @@ package org.schabi.newpipe.settings -import android.os.Build import android.os.Bundle -import androidx.preference.Preference -import org.schabi.newpipe.R class NotificationSettingsFragment : BasePreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResourceRegistry() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key)) - colorizePref?.let { - preferenceScreen.removePreference(it) - } - } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 1ff7947fd..1158b3d83 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -12,28 +12,27 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.RadioButton; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.grack.nanojson.JsonStringWriter; import com.grack.nanojson.JsonWriter; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.DialogEditTextBinding; +import org.schabi.newpipe.databinding.FragmentInstanceListBinding; +import org.schabi.newpipe.databinding.ItemInstanceBinding; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.PeertubeHelper; @@ -41,7 +40,6 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; import java.util.Collections; -import java.util.List; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; @@ -50,12 +48,11 @@ import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; public class PeertubeInstanceListFragment extends Fragment { - private final List instanceList = new ArrayList<>(); private PeertubeInstance selectedInstance; private String savedInstanceListKey; private InstanceListAdapter instanceListAdapter; - private ProgressBar progressBar; + private FragmentInstanceListBinding binding; private SharedPreferences sharedPreferences; private CompositeDisposable disposables = new CompositeDisposable(); @@ -71,7 +68,6 @@ public class PeertubeInstanceListFragment extends Fragment { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); savedInstanceListKey = getString(R.string.peertube_instance_list_key); selectedInstance = PeertubeHelper.getCurrentInstance(); - updateInstanceList(); setHasOptionsMenu(true); } @@ -79,7 +75,8 @@ public class PeertubeInstanceListFragment extends Fragment { @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_instance_list, container, false); + binding = FragmentInstanceListBinding.inflate(inflater, container, false); + return binding.getRoot(); } @Override @@ -87,26 +84,17 @@ public class PeertubeInstanceListFragment extends Fragment { @Nullable final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); - initViews(rootView); - } - - private void initViews(@NonNull final View rootView) { - final TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV); - instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, + binding.instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, getString(R.string.peertube_instance_list_url))); - - initButton(rootView); - - final RecyclerView listInstances = rootView.findViewById(R.id.instances); - listInstances.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.addInstanceButton.setOnClickListener(v -> showAddItemDialog(requireContext())); + binding.instances.setLayoutManager(new LinearLayoutManager(requireContext())); final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(listInstances); + itemTouchHelper.attachToRecyclerView(binding.instances); instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper); - listInstances.setAdapter(instanceListAdapter); - - progressBar = rootView.findViewById(R.id.loading_progress_bar); + binding.instances.setAdapter(instanceListAdapter); + instanceListAdapter.submitList(PeertubeHelper.getInstanceList(requireContext())); } @Override @@ -131,6 +119,12 @@ public class PeertubeInstanceListFragment extends Fragment { disposables = null; } + @Override + public void onDestroyView() { + binding = null; + super.onDestroyView(); + } + /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -156,11 +150,6 @@ public class PeertubeInstanceListFragment extends Fragment { // Utils //////////////////////////////////////////////////////////////////////////*/ - private void updateInstanceList() { - instanceList.clear(); - instanceList.addAll(PeertubeHelper.getInstanceList(requireContext())); - } - private void selectInstance(final PeertubeInstance instance) { selectedInstance = PeertubeHelper.selectInstance(instance, requireContext()); sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); @@ -168,7 +157,7 @@ public class PeertubeInstanceListFragment extends Fragment { private void saveChanges() { final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); - for (final PeertubeInstance instance : instanceList) { + for (final PeertubeInstance instance : instanceListAdapter.getCurrentList()) { jsonWriter.object(); jsonWriter.value("name", instance.getName()); jsonWriter.value("url", instance.getUrl()); @@ -179,28 +168,21 @@ public class PeertubeInstanceListFragment extends Fragment { } private void restoreDefaults() { - new AlertDialog.Builder(requireContext()) + final Context context = requireContext(); + new AlertDialog.Builder(context) .setTitle(R.string.restore_defaults) .setMessage(R.string.restore_defaults_confirmation) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which) -> { sharedPreferences.edit().remove(savedInstanceListKey).apply(); selectInstance(PeertubeInstance.DEFAULT_INSTANCE); - updateInstanceList(); - instanceListAdapter.notifyDataSetChanged(); + instanceListAdapter.submitList(PeertubeHelper.getInstanceList(context)); }) .show(); } - private void initButton(final View rootView) { - final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton); - fab.setOnClickListener(v -> - showAddItemDialog(requireContext())); - } - private void showAddItemDialog(final Context c) { - final DialogEditTextBinding dialogBinding - = DialogEditTextBinding.inflate(getLayoutInflater()); + final var dialogBinding = DialogEditTextBinding.inflate(getLayoutInflater()); dialogBinding.dialogEditText.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); dialogBinding.dialogEditText.setHint(R.string.peertube_instance_add_help); @@ -222,17 +204,17 @@ public class PeertubeInstanceListFragment extends Fragment { if (cleanUrl == null) { return; } - progressBar.setVisibility(View.VISIBLE); + binding.loadingProgressBar.setVisibility(View.VISIBLE); final Disposable disposable = Single.fromCallable(() -> { final PeertubeInstance instance = new PeertubeInstance(cleanUrl); instance.fetchInstanceMetaData(); return instance; }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) .subscribe((instance) -> { - progressBar.setVisibility(View.GONE); + binding.loadingProgressBar.setVisibility(View.GONE); add(instance); }, e -> { - progressBar.setVisibility(View.GONE); + binding.loadingProgressBar.setVisibility(View.GONE); Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show(); }); @@ -255,7 +237,7 @@ public class PeertubeInstanceListFragment extends Fragment { return null; } // only allow if not already exists - for (final PeertubeInstance instance : instanceList) { + for (final PeertubeInstance instance : instanceListAdapter.getCurrentList()) { if (instance.getUrl().equals(cleanUrl)) { Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show(); @@ -266,8 +248,9 @@ public class PeertubeInstanceListFragment extends Fragment { } private void add(final PeertubeInstance instance) { - instanceList.add(instance); - instanceListAdapter.notifyDataSetChanged(); + final var list = new ArrayList<>(instanceListAdapter.getCurrentList()); + list.add(instance); + instanceListAdapter.submitList(list); } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { @@ -281,8 +264,7 @@ public class PeertubeInstanceListFragment extends Fragment { final long msSinceStartScroll) { final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int minimumAbsVelocity = Math.max(12, - Math.abs(standardSpeed)); + final int minimumAbsVelocity = Math.max(12, Math.abs(standardSpeed)); return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); } @@ -316,17 +298,19 @@ public class PeertubeInstanceListFragment extends Fragment { final int swipeDir) { final int position = viewHolder.getBindingAdapterPosition(); // do not allow swiping the selected instance - if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) { + if (instanceListAdapter.getCurrentList().get(position).getUrl() + .equals(selectedInstance.getUrl())) { instanceListAdapter.notifyItemChanged(position); return; } - instanceList.remove(position); - instanceListAdapter.notifyItemRemoved(position); + final var list = new ArrayList<>(instanceListAdapter.getCurrentList()); + list.remove(position); - if (instanceList.isEmpty()) { - instanceList.add(selectedInstance); - instanceListAdapter.notifyItemInserted(0); + if (list.isEmpty()) { + list.add(selectedInstance); } + + instanceListAdapter.submitList(list); } }; } @@ -336,96 +320,94 @@ public class PeertubeInstanceListFragment extends Fragment { //////////////////////////////////////////////////////////////////////////*/ private class InstanceListAdapter - extends RecyclerView.Adapter { + extends ListAdapter { private final LayoutInflater inflater; private final ItemTouchHelper itemTouchHelper; private RadioButton lastChecked; InstanceListAdapter(final Context context, final ItemTouchHelper itemTouchHelper) { + super(new PeertubeInstanceCallback()); this.itemTouchHelper = itemTouchHelper; this.inflater = LayoutInflater.from(context); } public void swapItems(final int fromPosition, final int toPosition) { - Collections.swap(instanceList, fromPosition, toPosition); - notifyItemMoved(fromPosition, toPosition); + final var list = new ArrayList<>(getCurrentList()); + Collections.swap(list, fromPosition, toPosition); + submitList(list); } @NonNull @Override public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - final View view = inflater.inflate(R.layout.item_instance, parent, false); - return new InstanceListAdapter.TabViewHolder(view); + return new InstanceListAdapter.TabViewHolder(ItemInstanceBinding.inflate(inflater, + parent, false)); } @Override public void onBindViewHolder(@NonNull final InstanceListAdapter.TabViewHolder holder, final int position) { - holder.bind(position, holder); - } - - @Override - public int getItemCount() { - return instanceList.size(); + holder.bind(position); } class TabViewHolder extends RecyclerView.ViewHolder { - private final AppCompatImageView instanceIconView; - private final TextView instanceNameView; - private final TextView instanceUrlView; - private final RadioButton instanceRB; - private final ImageView handle; + private final ItemInstanceBinding itemBinding; - TabViewHolder(final View itemView) { - super(itemView); - - instanceIconView = itemView.findViewById(R.id.instanceIcon); - instanceNameView = itemView.findViewById(R.id.instanceName); - instanceUrlView = itemView.findViewById(R.id.instanceUrl); - instanceRB = itemView.findViewById(R.id.selectInstanceRB); - handle = itemView.findViewById(R.id.handle); + TabViewHolder(final ItemInstanceBinding binding) { + super(binding.getRoot()); + this.itemBinding = binding; } @SuppressLint("ClickableViewAccessibility") - void bind(final int position, final TabViewHolder holder) { - handle.setOnTouchListener(getOnTouchListener(holder)); - - final PeertubeInstance instance = instanceList.get(position); - instanceNameView.setText(instance.getName()); - instanceUrlView.setText(instance.getUrl()); - instanceRB.setOnCheckedChangeListener(null); - if (selectedInstance.getUrl().equals(instance.getUrl())) { - if (lastChecked != null && lastChecked != instanceRB) { - lastChecked.setChecked(false); - } - instanceRB.setChecked(true); - lastChecked = instanceRB; - } - instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (isChecked) { - selectInstance(instance); - if (lastChecked != null && lastChecked != instanceRB) { - lastChecked.setChecked(false); - } - lastChecked = instanceRB; - } - }); - instanceIconView.setImageResource(R.drawable.ic_placeholder_peertube); - } - - @SuppressLint("ClickableViewAccessibility") - private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { - return (view, motionEvent) -> { + void bind(final int position) { + itemBinding.handle.setOnTouchListener((view, motionEvent) -> { if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { if (itemTouchHelper != null && getItemCount() > 1) { - itemTouchHelper.startDrag(item); + itemTouchHelper.startDrag(this); return true; } } return false; - }; + }); + + final PeertubeInstance instance = getItem(position); + itemBinding.instanceName.setText(instance.getName()); + itemBinding.instanceUrl.setText(instance.getUrl()); + itemBinding.selectInstanceRB.setOnCheckedChangeListener(null); + if (selectedInstance.getUrl().equals(instance.getUrl())) { + if (lastChecked != null && lastChecked != itemBinding.selectInstanceRB) { + lastChecked.setChecked(false); + } + itemBinding.selectInstanceRB.setChecked(true); + lastChecked = itemBinding.selectInstanceRB; + } + itemBinding.selectInstanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + selectInstance(instance); + if (lastChecked != null && lastChecked != itemBinding.selectInstanceRB) { + lastChecked.setChecked(false); + } + lastChecked = itemBinding.selectInstanceRB; + } + }); + itemBinding.instanceIcon.setImageResource(R.drawable.ic_placeholder_peertube); } } } + + private static class PeertubeInstanceCallback extends DiffUtil.ItemCallback { + @Override + public boolean areItemsTheSame(@NonNull final PeertubeInstance oldItem, + @NonNull final PeertubeInstance newItem) { + return oldItem.getUrl().equals(newItem.getUrl()); + } + + @Override + public boolean areContentsTheSame(@NonNull final PeertubeInstance oldItem, + @NonNull final PeertubeInstance newItem) { + return oldItem.getName().equals(newItem.getName()) + && oldItem.getUrl().equals(newItem.getUrl()); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/PlayerNotificationSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/PlayerNotificationSettingsFragment.kt index 3549bff42..7d95433a4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PlayerNotificationSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/PlayerNotificationSettingsFragment.kt @@ -1,19 +1,9 @@ package org.schabi.newpipe.settings -import android.os.Build import android.os.Bundle -import androidx.preference.Preference -import org.schabi.newpipe.R class PlayerNotificationSettingsFragment : BasePreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResourceRegistry() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key)) - colorizePref?.let { - preferenceScreen.removePreference(it) - } - } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java index 8924ecbe1..b1e2c04eb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.settings; import android.content.Context; import android.content.SharedPreferences; -import android.os.Build; import android.util.Log; import androidx.preference.PreferenceManager; @@ -71,12 +70,12 @@ public final class SettingMigrations { // and standard way to access folders and files to be used consistently everywhere. // We reset the setting to its default value, i.e. "use SAF", since now there are no // more issues with SAF and users should use that one instead of the old - // NoNonsenseFilePicker. SAF does not work on KitKat and below, though, so the setting - // is set to false in that case. Also, there's a bug on FireOS in which SAF open/close + // NoNonsenseFilePicker. Also, there's a bug on FireOS in which SAF open/close // dialogs cannot be confirmed with a remote (see #6455). - sp.edit().putBoolean(context.getString(R.string.storage_use_saf), - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && !DeviceUtils.isFireTv()).apply(); + sp.edit().putBoolean( + context.getString(R.string.storage_use_saf), + !DeviceUtils.isFireTv() + ).apply(); } }; diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index 1043e88c2..f1f63ffdf 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -9,19 +9,19 @@ import org.schabi.newpipe.NewVersionWorker; import org.schabi.newpipe.R; public class UpdateSettingsFragment extends BasePreferenceFragment { - private final Preference.OnPreferenceChangeListener updatePreferenceChange - = (preference, checkForUpdates) -> { + private final Preference.OnPreferenceChangeListener updatePreferenceChange = (p, nVal) -> { + final boolean checkForUpdates = (boolean) nVal; defaultPreferences.edit() - .putBoolean(getString(R.string.update_app_key), (boolean) checkForUpdates).apply(); + .putBoolean(getString(R.string.update_app_key), checkForUpdates) + .apply(); - if ((boolean) checkForUpdates) { + if (checkForUpdates) { checkNewVersionNow(); } return true; }; - private final Preference.OnPreferenceClickListener manualUpdateClick - = preference -> { + private final Preference.OnPreferenceClickListener manualUpdateClick = preference -> { Toast.makeText(getContext(), R.string.checking_updates_toast, Toast.LENGTH_SHORT).show(); checkNewVersionNow(); return true; diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java index 798d299c0..1770685e4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.settings.custom; +import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -23,16 +25,17 @@ import androidx.core.graphics.drawable.DrawableCompat; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; -import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.NotificationConstants; +import org.schabi.newpipe.player.notification.NotificationConstants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; import java.util.List; +import java.util.stream.IntStream; public class NotificationActionsPreference extends Preference { @@ -61,7 +64,9 @@ public class NotificationActionsPreference extends Preference { public void onDetached() { super.onDetached(); saveChanges(); - getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION)); + // set package to this app's package to prevent the intent from being seen outside + getContext().sendBroadcast(new Intent(ACTION_RECREATE_NOTIFICATION) + .setPackage(App.PACKAGE_NAME)); } @@ -70,13 +75,11 @@ public class NotificationActionsPreference extends Preference { //////////////////////////////////////////////////////////////////////////// private void setupActions(@NonNull final View view) { - compactSlots = - NotificationConstants.getCompactSlotsFromPreferences( - getContext(), getSharedPreferences(), 5); - notificationSlots = new NotificationSlot[5]; - for (int i = 0; i < 5; i++) { - notificationSlots[i] = new NotificationSlot(i, view); - } + compactSlots = NotificationConstants.getCompactSlotsFromPreferences(getContext(), + getSharedPreferences(), 5); + notificationSlots = IntStream.range(0, 5) + .mapToObj(i -> new NotificationSlot(i, view)) + .toArray(NotificationSlot[]::new); } @@ -218,7 +221,7 @@ public class NotificationActionsPreference extends Preference { final int color = ThemeHelper.resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary); drawable = DrawableCompat.wrap(drawable).mutate(); - DrawableCompat.setTint(drawable, color); + drawable.setTint(color); radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, drawable, null); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java index 7c231cafb..ea45c68d2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java @@ -31,7 +31,7 @@ public class PreferenceFuzzySearchFunction // Specific search - Used for determining order of search results // Calculate a score based on specific search fields .map(item -> new FuzzySearchSpecificDTO(item, keyword)) - .sorted(Comparator.comparing(FuzzySearchSpecificDTO::getScore).reversed()) + .sorted(Comparator.comparingDouble(FuzzySearchSpecificDTO::getScore).reversed()) .map(FuzzySearchSpecificDTO::getItem) // Limit the amount of search results .limit(20); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java index 1f507c7f1..b925e8b5f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceParser.java @@ -9,13 +9,13 @@ import androidx.annotation.Nullable; import androidx.annotation.XmlRes; import androidx.preference.PreferenceManager; +import org.schabi.newpipe.util.Localization; import org.xmlpull.v1.XmlPullParser; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * Parses the corresponding preference-file(s). @@ -54,7 +54,7 @@ public class PreferenceParser { if (xpp.getEventType() == XmlPullParser.START_TAG) { final PreferenceSearchItem result = parseSearchResult( xpp, - joinBreadcrumbs(breadcrumbs), + Localization.concatenateStrings(" > ", breadcrumbs), resId ); @@ -82,12 +82,6 @@ public class PreferenceParser { return results; } - private String joinBreadcrumbs(final List breadcrumbs) { - return breadcrumbs.stream() - .filter(crumb -> !TextUtils.isEmpty(crumb)) - .collect(Collectors.joining(" > ")); - } - private String getAttribute( final XmlPullParser xpp, @NonNull final String attribute diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java index 02fbf9577..d6e2021a1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java @@ -1,54 +1,48 @@ package org.schabi.newpipe.settings.preferencesearch; -import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.databinding.SettingsPreferencesearchListItemResultBinding; -import java.util.ArrayList; -import java.util.List; import java.util.function.Consumer; class PreferenceSearchAdapter - extends RecyclerView.Adapter { - private List dataset = new ArrayList<>(); + extends ListAdapter { private Consumer onItemClickListener; + PreferenceSearchAdapter() { + super(new PreferenceCallback()); + } + @NonNull @Override - public PreferenceViewHolder onCreateViewHolder( - @NonNull final ViewGroup parent, - final int viewType - ) { - return new PreferenceViewHolder( - SettingsPreferencesearchListItemResultBinding.inflate( - LayoutInflater.from(parent.getContext()), - parent, - false)); + public PreferenceViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, + final int viewType) { + return new PreferenceViewHolder(SettingsPreferencesearchListItemResultBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false)); } @Override - public void onBindViewHolder( - @NonNull final PreferenceViewHolder holder, - final int position - ) { - final PreferenceSearchItem item = dataset.get(position); + public void onBindViewHolder(@NonNull final PreferenceViewHolder holder, final int position) { + final PreferenceSearchItem item = getItem(position); holder.binding.title.setText(item.getTitle()); - if (TextUtils.isEmpty(item.getSummary())) { + if (item.getSummary().isEmpty()) { holder.binding.summary.setVisibility(View.GONE); } else { holder.binding.summary.setVisibility(View.VISIBLE); holder.binding.summary.setText(item.getSummary()); } - if (TextUtils.isEmpty(item.getBreadcrumbs())) { + if (item.getBreadcrumbs().isEmpty()) { holder.binding.breadcrumbs.setVisibility(View.GONE); } else { holder.binding.breadcrumbs.setVisibility(View.VISIBLE); @@ -62,16 +56,6 @@ class PreferenceSearchAdapter }); } - void setContent(final List items) { - dataset = new ArrayList<>(items); - this.notifyDataSetChanged(); - } - - @Override - public int getItemCount() { - return dataset.size(); - } - void setOnItemClickListener(final Consumer onItemClickListener) { this.onItemClickListener = onItemClickListener; } @@ -84,4 +68,19 @@ class PreferenceSearchAdapter this.binding = binding; } } + + private static class PreferenceCallback extends DiffUtil.ItemCallback { + @Override + public boolean areItemsTheSame(@NonNull final PreferenceSearchItem oldItem, + @NonNull final PreferenceSearchItem newItem) { + return oldItem.getKey().equals(newItem.getKey()); + } + + @Override + public boolean areContentsTheSame(@NonNull final PreferenceSearchItem oldItem, + @NonNull final PreferenceSearchItem newItem) { + return oldItem.getAllRelevantSearchFields().equals(newItem + .getAllRelevantSearchFields()); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index a445ea309..1ded181c8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -3,8 +3,6 @@ package org.schabi.newpipe.settings.preferencesearch; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -12,9 +10,9 @@ import java.util.stream.Stream; public class PreferenceSearchConfiguration { private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction(); - private final List parserIgnoreElements = Collections.singletonList( + private final List parserIgnoreElements = List.of( PreferenceCategory.class.getSimpleName()); - private final List parserContainerElements = Arrays.asList( + private final List parserContainerElements = List.of( PreferenceCategory.class.getSimpleName(), PreferenceScreen.class.getSimpleName()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index 308abbc4e..9d169d660 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.settings.preferencesearch; import android.os.Bundle; -import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -13,7 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding; -import java.util.ArrayList; import java.util.List; /** @@ -54,13 +52,8 @@ public class PreferenceSearchFragment extends Fragment { return; } - final List results = - !TextUtils.isEmpty(keyword) - ? searcher.searchFor(keyword) - : new ArrayList<>(); - - adapter.setContent(new ArrayList<>(results)); - + final List results = searcher.searchFor(keyword); + adapter.submitList(results); setEmptyViewShown(results.isEmpty()); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java index 98d2a5d84..33856326c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.settings.preferencesearch; import androidx.annotation.NonNull; import androidx.annotation.XmlRes; -import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -92,11 +91,7 @@ public class PreferenceSearchItem { } public List getAllRelevantSearchFields() { - return Arrays.asList( - getTitle(), - getSummary(), - getEntries(), - getBreadcrumbs()); + return List.of(getTitle(), getSummary(), getEntries(), getBreadcrumbs()); } @NonNull diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java index 418a3ea46..7eae5c128 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultHighlighter.java @@ -6,7 +6,6 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; @@ -65,8 +64,7 @@ public final class PreferenceSearchResultHighlighter { recyclerView.findViewHolderForAdapterPosition(position); if (holder != null) { final Drawable background = holder.itemView.getBackground(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && background instanceof RippleDrawable) { + if (background instanceof RippleDrawable) { showRippleAnimation((RippleDrawable) background); return; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java index 176dc5d14..b3efc8dd1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearcher.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.settings.preferencesearch; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -21,7 +22,7 @@ public class PreferenceSearcher { List searchFor(final String keyword) { if (TextUtils.isEmpty(keyword)) { - return new ArrayList<>(); + return Collections.emptyList(); } return configuration.getSearcher() diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java index 057ca50f0..32f25ccbd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java @@ -10,8 +10,6 @@ import com.grack.nanojson.JsonStringWriter; import com.grack.nanojson.JsonWriter; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -20,11 +18,10 @@ import java.util.List; public final class TabsJsonHelper { private static final String JSON_TABS_ARRAY_KEY = "tabs"; - private static final List FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList( - Arrays.asList( - Tab.Type.DEFAULT_KIOSK.getTab(), - Tab.Type.SUBSCRIPTIONS.getTab(), - Tab.Type.BOOKMARKS.getTab())); + private static final List FALLBACK_INITIAL_TABS_LIST = List.of( + Tab.Type.DEFAULT_KIOSK.getTab(), + Tab.Type.SUBSCRIPTIONS.getTab(), + Tab.Type.BOOKMARKS.getTab()); private TabsJsonHelper() { } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index dc6e29d7d..68225fbab 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -82,8 +82,8 @@ public class DataReader { public long readLong() throws IOException { primitiveRead(LONG_SIZE); - final long high - = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; + final long high = + primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; final long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7]; return high << 32 | low; } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 889cc85e6..807f190b4 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -307,8 +307,8 @@ public class Mp4FromDashWriter { outWrite(makeMdat(totalSampleSize, is64)); final int[] sampleIndex = new int[readers.length]; - final int[] sizes - = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; + final int[] sizes = + new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; final int[] sync = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; int written = readers.length; diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java index 8253ad6af..678974cce 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -348,8 +348,7 @@ public class WebMReader { ensure(elemTrackEntry); } - final WebMTrack[] entries = new WebMTrack[trackEntries.size()]; - trackEntries.toArray(entries); + final WebMTrack[] entries = trackEntries.toArray(new WebMTrack[0]); for (final WebMTrack entry : entries) { switch (entry.trackType) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java index feca89f02..48ae54284 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.provider.DocumentsContract; import androidx.annotation.NonNull; @@ -53,10 +52,6 @@ public class StoredDirectoryHelper { throw new IOException(e); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - throw new IOException("Storage Access Framework with Directory API is not available"); - } - this.docTree = DocumentFile.fromTreeUri(context, path); if (this.docTree == null) { @@ -73,7 +68,7 @@ public class StoredDirectoryHelper { final String[] filename = splitFilename(name); final String lcFilename = filename[0].toLowerCase(); - if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (docTree == null) { for (final File file : ioTree.listFiles()) { addIfStartWith(matches, lcFilename, file.getName()); } @@ -277,7 +272,7 @@ public class StoredDirectoryHelper { */ static DocumentFile findFileSAFHelper(@Nullable final Context context, final DocumentFile tree, final String filename) { - if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (context == null) { return tree.findFile(filename); // warning: this is very slow } diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index 9fe4a9340..1f0c91456 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.streams.io; -import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -74,7 +73,6 @@ public class StoredFileHelper implements Serializable { this.tag = tag; } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) StoredFileHelper(@Nullable final Context context, final DocumentFile tree, final String filename, final String mime, final boolean safe) throws IOException { @@ -124,7 +122,6 @@ public class StoredFileHelper implements Serializable { this.srcType = mime; } - @TargetApi(Build.VERSION_CODES.KITKAT) public StoredFileHelper(final Context context, @Nullable final Uri parent, @NonNull final Uri path, final String tag) throws IOException { this.tag = tag; diff --git a/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java b/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java deleted file mode 100644 index d970ec472..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.schabi.newpipe.util; - -import android.text.TextUtils; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -public final class CookieUtils { - private CookieUtils() { - } - - public static String concatCookies(final Collection cookieStrings) { - final Set cookieSet = new HashSet<>(); - for (final String cookies : cookieStrings) { - cookieSet.addAll(splitCookies(cookies)); - } - return TextUtils.join("; ", cookieSet).trim(); - } - - public static Set splitCookies(final String cookies) { - return new HashSet<>(Arrays.asList(cookies.split("; *"))); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index a4ff5ff19..3c20dc04b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -4,11 +4,14 @@ import android.app.UiModeManager; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Point; import android.os.BatteryManager; import android.os.Build; import android.provider.Settings; import android.util.TypedValue; import android.view.KeyEvent; +import android.view.WindowInsets; +import android.view.WindowManager; import androidx.annotation.Dimension; import androidx.annotation.NonNull; @@ -65,7 +68,7 @@ public final class DeviceUtils { boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class) .getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION || isFireTv() - || pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION); + || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); // from https://stackoverflow.com/a/58932366 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -77,10 +80,6 @@ public final class DeviceUtils { && pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET)); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - isTv = isTv || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); - } - DeviceUtils.isTV = isTv; return DeviceUtils.isTV; } @@ -131,11 +130,10 @@ public final class DeviceUtils { /** * Some devices have broken tunneled video playback but claim to support it. * See https://github.com/TeamNewPipe/NewPipe/issues/5911 - * @return false if Kitkat (does not support tunneling) or affected device + * @return false if affected device */ public static boolean shouldSupportMediaTunneling() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && !HI3798MV200 + return !HI3798MV200 && !CVT_MT5886_EU_1G && !REALTEKATV && !QM16XE_U; @@ -156,4 +154,18 @@ public final class DeviceUtils { Settings.Global.ANIMATOR_DURATION_SCALE, 1F) != 0F; } + + public static int getWindowHeight(@NonNull final WindowManager windowManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final var windowMetrics = windowManager.getCurrentWindowMetrics(); + final var windowInsets = windowMetrics.getWindowInsets(); + final var insets = windowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()); + return windowMetrics.getBounds().height() - (insets.top + insets.bottom); + } else { + final Point point = new Point(); + windowManager.getDefaultDisplay().getSize(point); + return point.y; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java index 20d8ce30c..d7fb39651 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java @@ -76,7 +76,7 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File public static class CustomFilePickerFragment extends FilePickerFragment { @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); } @@ -138,7 +138,7 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File } @Override - public void onLoadFinished(final Loader> loader, + public void onLoadFinished(@NonNull final Loader> loader, final SortedList data) { super.onLoadFinished(loader, data); layoutManager.scrollToPosition(0); diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index eabac8330..b3b7c1792 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -32,17 +31,16 @@ import java.util.stream.Collectors; public final class ListHelper { // Video format in order of quality. 0=lowest quality, n=highest quality private static final List VIDEO_FORMAT_QUALITY_RANKING = - Arrays.asList(MediaFormat.v3GPP, MediaFormat.WEBM, MediaFormat.MPEG_4); + List.of(MediaFormat.v3GPP, MediaFormat.WEBM, MediaFormat.MPEG_4); // Audio format in order of quality. 0=lowest quality, n=highest quality private static final List AUDIO_FORMAT_QUALITY_RANKING = - Arrays.asList(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); + List.of(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); // Audio format in order of efficiency. 0=most efficient, n=least efficient private static final List AUDIO_FORMAT_EFFICIENCY_RANKING = - Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); - // Use a HashSet for better performance - private static final Set HIGH_RESOLUTION_LIST = new HashSet<>( - Arrays.asList("1440p", "2160p")); + List.of(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); + // Use a Set for better performance + private static final Set HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p"); private ListHelper() { } @@ -176,8 +174,8 @@ public final class ListHelper { @Nullable final List videoOnlyStreams, final boolean ascendingOrder, final boolean preferVideoOnlyStreams) { - final SharedPreferences preferences - = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); final boolean showHigherResolutions = preferences.getBoolean( context.getString(R.string.show_higher_resolutions_key), false); @@ -214,8 +212,8 @@ public final class ListHelper { private static String computeDefaultResolution(final Context context, final int key, final int value) { - final SharedPreferences preferences - = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); // Load the preferred resolution otherwise the best available String resolution = preferences != null @@ -232,14 +230,15 @@ public final class ListHelper { } /** - * Return the index of the default stream in the list, based on the parameters - * defaultResolution and defaultFormat. + * Return the index of the default stream in the list, that will be sorted in the process, based + * on the parameters defaultResolution and defaultFormat. * * @param defaultResolution the default resolution to look for * @param bestResolutionKey key of the best resolution * @param defaultFormat the default format to look for - * @param videoStreams list of the video streams to check - * @return index of the default resolution&format + * @param videoStreams a mutable list of the video streams to check (it will be sorted in + * place) + * @return index of the default resolution&format in the sorted videoStreams */ static int getDefaultResolutionIndex(final String defaultResolution, final String bestResolutionKey, @@ -254,8 +253,8 @@ public final class ListHelper { return 0; } - final int defaultStreamIndex - = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); + final int defaultStreamIndex = + getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); // this is actually an error, // but maybe there is really no stream fitting to the default value. @@ -344,7 +343,10 @@ public final class ListHelper { */ private static List sortStreamList(final List videoStreams, final boolean ascendingOrder) { - final Comparator comparator = ListHelper::compareVideoStreamResolution; + // Compares the quality of two video streams. + final Comparator comparator = Comparator.nullsLast(Comparator + .comparing(VideoStream::getResolution, ListHelper::compareVideoStreamResolution) + .thenComparingInt(s -> VIDEO_FORMAT_QUALITY_RANKING.indexOf(s.getFormat()))); Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed()); return videoStreams; } @@ -361,8 +363,7 @@ public final class ListHelper { @Nullable final List audioStreams) { return getAudioIndexByHighestRank(format, audioStreams, // Compares descending (last = highest rank) - (s1, s2) -> compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_QUALITY_RANKING) - ); + getAudioStreamComparator(AUDIO_FORMAT_QUALITY_RANKING)); } /** @@ -375,11 +376,15 @@ public final class ListHelper { */ static int getMostCompactAudioIndex(@Nullable final MediaFormat format, @Nullable final List audioStreams) { - return getAudioIndexByHighestRank(format, audioStreams, - // The "-" is important -> Compares ascending (first = highest rank) - (s1, s2) -> -compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_EFFICIENCY_RANKING) - ); + // The "reversed()" is important -> Compares ascending (first = highest rank) + getAudioStreamComparator(AUDIO_FORMAT_EFFICIENCY_RANKING).reversed()); + } + + private static Comparator getAudioStreamComparator( + final List formatRanking) { + return Comparator.nullsLast(Comparator.comparingInt(AudioStream::getAverageBitrate)) + .thenComparingInt(stream -> formatRanking.indexOf(stream.getFormat())); } /** @@ -446,8 +451,9 @@ public final class ListHelper { final String targetResolutionNoRefresh = targetResolution.replaceAll("p\\d+$", "p"); for (int idx = 0; idx < videoStreams.size(); idx++) { - final MediaFormat format - = targetFormat == null ? null : videoStreams.get(idx).getFormat(); + final MediaFormat format = targetFormat == null + ? null + : videoStreams.get(idx).getFormat(); final String resolution = videoStreams.get(idx).getResolution(); final String resolutionNoRefresh = resolution.replaceAll("p\\d+$", "p"); @@ -510,8 +516,8 @@ public final class ListHelper { private static MediaFormat getDefaultFormat(@NonNull final Context context, @StringRes final int defaultFormatKey, @StringRes final int defaultFormatValueKey) { - final SharedPreferences preferences - = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); final String defaultFormat = context.getString(defaultFormatValueKey); final String defaultFormatString = preferences.getString( @@ -544,28 +550,6 @@ public final class ListHelper { return format; } - // Compares the quality of two audio streams - private static int compareAudioStreamBitrate(final AudioStream streamA, - final AudioStream streamB, - final List formatRanking) { - if (streamA == null) { - return -1; - } - if (streamB == null) { - return 1; - } - if (streamA.getAverageBitrate() < streamB.getAverageBitrate()) { - return -1; - } - if (streamA.getAverageBitrate() > streamB.getAverageBitrate()) { - return 1; - } - - // Same bitrate and format - return formatRanking.indexOf(streamA.getFormat()) - - formatRanking.indexOf(streamB.getFormat()); - } - private static int compareVideoStreamResolution(@NonNull final String r1, @NonNull final String r2) { try { @@ -582,28 +566,6 @@ public final class ListHelper { } } - // Compares the quality of two video streams. - private static int compareVideoStreamResolution(final VideoStream streamA, - final VideoStream streamB) { - if (streamA == null) { - return -1; - } - if (streamB == null) { - return 1; - } - - final int resComp = compareVideoStreamResolution(streamA.getResolution(), - streamB.getResolution()); - if (resComp != 0) { - return resComp; - } - - // Same bitrate and format - return ListHelper.VIDEO_FORMAT_QUALITY_RANKING.indexOf(streamA.getFormat()) - - ListHelper.VIDEO_FORMAT_QUALITY_RANKING.indexOf(streamB.getFormat()); - } - - private static boolean isLimitingDataUsage(final Context context) { return getResolutionLimit(context) != null; } @@ -617,8 +579,8 @@ public final class ListHelper { private static String getResolutionLimit(@NonNull final Context context) { String resolutionLimit = null; if (isMeteredNetwork(context)) { - final SharedPreferences preferences - = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); final String defValue = context.getString(R.string.limit_data_usage_none_key); final String value = preferences.getString( context.getString(R.string.limit_mobile_data_usage_key), defValue); @@ -634,8 +596,8 @@ public final class ListHelper { * @return {@code true} if connected to a metered network */ public static boolean isMeteredNetwork(@NonNull final Context context) { - final ConnectivityManager manager - = ContextCompat.getSystemService(context, ConnectivityManager.class); + final ConnectivityManager manager = + ContextCompat.getSystemService(context, ConnectivityManager.class); if (manager == null || manager.getActiveNetworkInfo() == null) { return false; } diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index b222f6abf..e20955a76 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -13,6 +13,7 @@ import android.util.DisplayMetrics; import androidx.annotation.NonNull; import androidx.annotation.PluralsRes; import androidx.annotation.StringRes; +import androidx.core.math.MathUtils; import androidx.preference.PreferenceManager; import org.ocpsoft.prettytime.PrettyTime; @@ -31,6 +32,7 @@ import java.time.format.FormatStyle; import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; /* @@ -62,26 +64,14 @@ public final class Localization { @NonNull public static String concatenateStrings(final String... strings) { - return concatenateStrings(Arrays.asList(strings)); + return concatenateStrings(DOT_SEPARATOR, Arrays.asList(strings)); } @NonNull - public static String concatenateStrings(final List strings) { - if (strings.isEmpty()) { - return ""; - } - - final StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(strings.get(0)); - - for (int i = 1; i < strings.size(); i++) { - final String string = strings.get(i); - if (!TextUtils.isEmpty(string)) { - stringBuilder.append(DOT_SEPARATOR).append(strings.get(i)); - } - } - - return stringBuilder.toString(); + public static String concatenateStrings(final String delimiter, final List strings) { + return strings.stream() + .filter(string -> !TextUtils.isEmpty(string)) + .collect(Collectors.joining(delimiter)); } public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization( @@ -247,8 +237,7 @@ public final class Localization { // is not the responsibility of this method handle long numbers // (it probably will fall in the "other" category, // or some language have some specific rule... then we have to change it) - final int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE - : count < Integer.MIN_VALUE ? Integer.MIN_VALUE : (int) count; + final int safeCount = (int) MathUtils.clamp(count, Integer.MIN_VALUE, Integer.MAX_VALUE); return context.getResources().getQuantityString(pluralId, safeCount, formattedCount); } @@ -358,19 +347,4 @@ public final class Localization { private static double round(final double value, final int places) { return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue(); } - - /** - * Workaround to match normalized captions like english to English or deutsch to Deutsch. - * @param list the list to search into - * @param toFind the string to look for - * @return whether the string was found or not - */ - public static boolean containsCaseInsensitive(final List list, final String toFind) { - for (final String i : list) { - if (i.equalsIgnoreCase(toFind)) { - return true; - } - } - return false; - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index c40b1a430..3b2c52691 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -50,10 +50,10 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; -import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.MainPlayer.PlayerType; +import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayQueueActivity; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -91,7 +91,7 @@ public final class NavigationHelper { intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); } } - intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); + intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent()); intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); return intent; @@ -163,8 +163,8 @@ public final class NavigationHelper { Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); - intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal()); + final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); + intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent()); ContextCompat.startForegroundService(context, intent); } @@ -174,8 +174,8 @@ public final class NavigationHelper { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); - final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); - intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal()); + final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); + intent.putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO.valueForIntent()); ContextCompat.startForegroundService(context, intent); } @@ -184,17 +184,17 @@ public final class NavigationHelper { final PlayQueue queue, final PlayerType playerType) { Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); - final Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue); + final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue); - intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal()); + intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent()); ContextCompat.startForegroundService(context, intent); } public static void enqueueOnPlayer(final Context context, final PlayQueue queue) { PlayerType playerType = PlayerHolder.getInstance().getType(); - if (!PlayerHolder.getInstance().isPlayerOpen()) { + if (playerType == null) { Log.e(TAG, "Enqueueing but no player is open; defaulting to background player"); - playerType = MainPlayer.PlayerType.AUDIO; + playerType = PlayerType.AUDIO; } enqueueOnPlayer(context, queue, playerType); @@ -203,14 +203,14 @@ public final class NavigationHelper { /* ENQUEUE NEXT */ public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) { PlayerType playerType = PlayerHolder.getInstance().getType(); - if (!PlayerHolder.getInstance().isPlayerOpen()) { + if (playerType == null) { Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player"); - playerType = MainPlayer.PlayerType.AUDIO; + playerType = PlayerType.AUDIO; } Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show(); - final Intent intent = getPlayerEnqueueNextIntent(context, MainPlayer.class, queue); + final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue); - intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal()); + intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent()); ContextCompat.startForegroundService(context, intent); } @@ -414,14 +414,14 @@ public final class NavigationHelper { final boolean switchingPlayers) { final boolean autoPlay; - @Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); - if (!PlayerHolder.getInstance().isPlayerOpen()) { + @Nullable final PlayerType playerType = PlayerHolder.getInstance().getType(); + if (playerType == null) { // no player open autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); } else if (switchingPlayers) { // switching player to main player autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state - } else if (playerType == MainPlayer.PlayerType.VIDEO) { + } else if (playerType == PlayerType.MAIN) { // opening new stream while already playing in main player autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); } else { @@ -436,7 +436,7 @@ public final class NavigationHelper { // Situation when user switches from players to main player. All needed data is // here, we can start watching (assuming newQueue equals playQueue). // Starting directly in fullscreen if the previous player type was popup. - detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP + detailFragment.openVideoPlayer(playerType == PlayerType.POPUP || PlayerHelper.isStartMainPlayerFullscreenEnabled(context)); } else { detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); diff --git a/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java index 5f44cab8b..ae8d86af1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java +++ b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java @@ -2,15 +2,14 @@ package org.schabi.newpipe.util; import androidx.recyclerview.widget.RecyclerView; -public abstract class OnClickGesture { +public interface OnClickGesture { + void selected(T selectedItem); - public abstract void selected(T selectedItem); - - public void held(final T selectedItem) { + default void held(final T selectedItem) { // Optional gesture } - public void drag(final T selectedItem, final RecyclerView.ViewHolder viewHolder) { + default void drag(final T selectedItem, final RecyclerView.ViewHolder viewHolder) { // Optional gesture } } diff --git a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java index dcc39eccf..34f99d262 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java @@ -17,7 +17,6 @@ import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public final class PeertubeHelper { @@ -29,7 +28,7 @@ public final class PeertubeHelper { final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key); final String savedJson = sharedPreferences.getString(savedInstanceListKey, null); if (null == savedJson) { - return Collections.singletonList(getCurrentInstance()); + return List.of(getCurrentInstance()); } try { @@ -45,17 +44,16 @@ public final class PeertubeHelper { } return result; } catch (final JsonParserException e) { - return Collections.singletonList(getCurrentInstance()); + return List.of(getCurrentInstance()); } - } public static PeertubeInstance selectInstance(final PeertubeInstance instance, final Context context) { final SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(context); - final String selectedInstanceKey - = context.getString(R.string.peertube_selected_instance_key); + final String selectedInstanceKey = + context.getString(R.string.peertube_selected_instance_key); final JsonStringWriter jsonWriter = JsonWriter.string().object(); jsonWriter.value("name", instance.getName()); jsonWriter.value("url", instance.getUrl()); diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 160eb59cd..f3151ec8b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -37,7 +37,6 @@ public final class PermissionHelper { return checkWriteStoragePermissions(activity, requestCode); } - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) public static boolean checkReadStoragePermissions(final Activity activity, final int requestCode) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) @@ -123,8 +122,8 @@ public final class PermissionHelper { } public static void showPopupEnablementToast(final Context context) { - final Toast toast - = Toast.makeText(context, R.string.msg_popup_permission, Toast.LENGTH_LONG); + final Toast toast = + Toast.makeText(context, R.string.msg_popup_permission, Toast.LENGTH_LONG); final TextView messageView = toast.getView().findViewById(android.R.id.message); if (messageView != null) { messageView.setGravity(Gravity.CENTER); diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java index aabc459d0..2e781631e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -1,11 +1,12 @@ package org.schabi.newpipe.util; +import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; +import android.util.Log; import androidx.annotation.Nullable; @@ -14,7 +15,6 @@ import com.squareup.picasso.LruCache; import com.squareup.picasso.OkHttp3Downloader; import com.squareup.picasso.Picasso; import com.squareup.picasso.RequestCreator; -import com.squareup.picasso.Target; import com.squareup.picasso.Transformation; import org.schabi.newpipe.R; @@ -22,14 +22,13 @@ import org.schabi.newpipe.R; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import okhttp3.OkHttpClient; public final class PicassoHelper { - public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG"; - private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY - = "PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"; + private static final String TAG = PicassoHelper.class.getSimpleName(); + private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY = + "PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"; private PicassoHelper() { } @@ -97,33 +96,44 @@ public final class PicassoHelper { public static RequestCreator loadAvatar(final String url) { - return loadImageDefault(url, R.drawable.buddy); + return loadImageDefault(url, R.drawable.placeholder_person); } public static RequestCreator loadThumbnail(final String url) { - return loadImageDefault(url, R.drawable.dummy_thumbnail); + return loadImageDefault(url, R.drawable.placeholder_thumbnail_video); + } + + public static RequestCreator loadDetailsThumbnail(final String url) { + return loadImageDefault(url, R.drawable.placeholder_thumbnail_video, false); } public static RequestCreator loadBanner(final String url) { - return loadImageDefault(url, R.drawable.channel_banner); + return loadImageDefault(url, R.drawable.placeholder_channel_banner); } public static RequestCreator loadPlaylistThumbnail(final String url) { - return loadImageDefault(url, R.drawable.dummy_thumbnail_playlist); + return loadImageDefault(url, R.drawable.placeholder_thumbnail_playlist); } public static RequestCreator loadSeekbarThumbnailPreview(final String url) { return picassoInstance.load(url); } + public static RequestCreator loadNotificationIcon(final String url) { + return loadImageDefault(url, R.drawable.ic_newpipe_triangle_white); + } + public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) { // scale down the notification thumbnail for performance return PicassoHelper.loadThumbnail(url) - .tag(PLAYER_THUMBNAIL_TAG) .transform(new Transformation() { @Override public Bitmap transform(final Bitmap source) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - transform() called"); + } + final float notificationThumbnailWidth = Math.min( context.getResources() .getDimension(R.dimen.player_notification_thumbnail_width), @@ -166,38 +176,26 @@ public final class PicassoHelper { return picassoCache.get(imageUrl + "\n"); } - public static void loadNotificationIcon(final String url, - final Consumer bitmapConsumer) { - loadImageDefault(url, R.drawable.ic_newpipe_triangle_white) - .into(new Target() { - @Override - public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) { - bitmapConsumer.accept(bitmap); - } - - @Override - public void onBitmapFailed(final Exception e, final Drawable errorDrawable) { - bitmapConsumer.accept(null); - } - - @Override - public void onPrepareLoad(final Drawable placeHolderDrawable) { - // Nothing to do - } - }); - } - private static RequestCreator loadImageDefault(final String url, final int placeholderResId) { + return loadImageDefault(url, placeholderResId, true); + } + + private static RequestCreator loadImageDefault(final String url, final int placeholderResId, + final boolean showPlaceholderWhileLoading) { if (!shouldLoadImages || isBlank(url)) { return picassoInstance .load((String) null) .placeholder(placeholderResId) // show placeholder when no image should load .error(placeholderResId); } else { - return picassoInstance + final RequestCreator requestCreator = picassoInstance .load(url) - .error(placeholderResId); // don't show placeholder while loading, only on error + .error(placeholderResId); + if (showPlaceholderWhileLoading) { + requestCreator.placeholder(placeholderResId); + } + return requestCreator; } } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt index 21a9059e2..0c66cc6d4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -7,8 +7,6 @@ import org.schabi.newpipe.App import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification import org.schabi.newpipe.error.UserAction -import java.io.ByteArrayInputStream -import java.io.InputStream import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.cert.CertificateEncodingException @@ -47,10 +45,8 @@ object ReleaseVersionUtil { return "" } val x509cert = try { - val cert = signatures[0].toByteArray() - val input: InputStream = ByteArrayInputStream(cert) val cf = CertificateFactory.getInstance("X509") - cf.generateCertificate(input) as X509Certificate + cf.generateCertificate(signatures[0].toByteArray().inputStream()) as X509Certificate } catch (e: CertificateException) { showRequestError(app, e, "Certificate error") return "" diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index b13ae4a97..acd019ba0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -169,15 +169,6 @@ public final class ServiceHelper { } } - public static boolean isBeta(final StreamingService s) { - switch (s.getServiceInfo().getName()) { - case "YouTube": - return false; - default: - return true; - } - } - public static void initService(final Context context, final int serviceId) { if (serviceId == ServiceList.PeerTube.getServiceId()) { final SharedPreferences sharedPreferences = PreferenceManager diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java index 0c5f418b2..6e9ea7a47 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java @@ -97,10 +97,10 @@ public final class SparseItemUtil { * @param url url of the stream to load * @param callback callback to be called with the result */ - private static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context context, - final int serviceId, - @NonNull final String url, - final Consumer callback) { + public static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context context, + final int serviceId, + @NonNull final String url, + final Consumer callback) { Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show(); ExtractorHelper.getStreamInfo(serviceId, url, false) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java index 6ebdaee02..3c901aacb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java @@ -46,8 +46,8 @@ import java.util.concurrent.ConcurrentHashMap; */ public final class StateSaver { public static final String KEY_SAVED_STATE = "key_saved_state"; - private static final ConcurrentHashMap> STATE_OBJECTS_HOLDER - = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> STATE_OBJECTS_HOLDER = + new ConcurrentHashMap<>(); private static final String TAG = "StateSaver"; private static final String CACHE_DIR_NAME = "state_cache"; private static String cacheDirPath; @@ -107,8 +107,8 @@ public final class StateSaver { } try { - Queue savedObjects - = STATE_OBJECTS_HOLDER.remove(savedState.getPrefixFileSaved()); + Queue savedObjects = + STATE_OBJECTS_HOLDER.remove(savedState.getPrefixFileSaved()); if (savedObjects != null) { writeRead.readFrom(savedObjects); if (MainActivity.DEBUG) { diff --git a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java deleted file mode 100644 index 05e69408a..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.schabi.newpipe.util; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; - -import android.util.Log; - - -/** - * This is an extension of the SSLSocketFactory which enables TLS 1.2 and 1.1. - * Created for usage on Android 4.1-4.4 devices, which haven't enabled those by default. - */ -public class TLSSocketFactoryCompat extends SSLSocketFactory { - - private static final String TAG = "TLSSocketFactoryCom"; - - private static TLSSocketFactoryCompat instance = null; - - private final SSLSocketFactory internalSSLSocketFactory; - - public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmException { - final SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, null, null); - internalSSLSocketFactory = context.getSocketFactory(); - } - - public static TLSSocketFactoryCompat getInstance() - throws NoSuchAlgorithmException, KeyManagementException { - if (instance != null) { - return instance; - } - instance = new TLSSocketFactoryCompat(); - return instance; - } - - public static void setAsDefault() { - try { - HttpsURLConnection.setDefaultSSLSocketFactory(getInstance()); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - Log.e(TAG, "Unable to setAsDefault", e); - } - } - - @Override - public String[] getDefaultCipherSuites() { - return internalSSLSocketFactory.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return internalSSLSocketFactory.getSupportedCipherSuites(); - } - - @Override - public Socket createSocket() throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); - } - - @Override - public Socket createSocket(final Socket s, final String host, final int port, - final boolean autoClose) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket(final String host, final int port) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(final String host, final int port, final InetAddress localHost, - final int localPort) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket( - host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(final InetAddress host, final int port) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(final InetAddress address, final int port, - final InetAddress localAddress, final int localPort) - throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket( - address, port, localAddress, localPort)); - } - - private Socket enableTLSOnSocket(final Socket socket) { - if (socket instanceof SSLSocket) { - ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); - } - return socket; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index b8e3a86ed..389af80ee 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -244,6 +244,22 @@ public final class ThemeHelper { return AppCompatResources.getDrawable(context, typedValue.resourceId); } + /** + * Gets a runtime dimen from the {@code android} package. Should be used for dimens for which + * normal accessing with {@code R.dimen.} is not available. + * + * @param context context + * @param name dimen resource name (e.g. navigation_bar_height) + * @return the obtained dimension, in pixels, or 0 if the resource could not be resolved + */ + public static int getAndroidDimenPx(@NonNull final Context context, final String name) { + final int resId = context.getResources().getIdentifier(name, "dimen", "android"); + if (resId <= 0) { + return 0; + } + return context.getResources().getDimensionPixelSize(resId); + } + private static String getSelectedThemeKey(final Context context) { final String themeKey = context.getString(R.string.theme_key); final String defaultTheme = context.getResources().getString(R.string.default_theme_value); diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java index 240341ab0..c46e6636d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java @@ -153,13 +153,13 @@ public final class InternalUrlsHandler { return false; } - final Single single - = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); + final Single single = + ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); disposables.add(single.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { - final PlayQueue playQueue - = new SinglePlayQueue(info, seconds * 1000L); + final PlayQueue playQueue = + new SinglePlayQueue(info, seconds * 1000L); NavigationHelper.playOnPopupPlayer(context, playQueue, false); }, throwable -> { if (DEBUG) { diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index 8324146fe..debeb902c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -185,17 +185,10 @@ public final class ShareUtils { } // Migrate any clip data and flags from the original intent. - final int permFlags; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); - } else { - permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } + final int permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); if (permFlags != 0) { ClipData targetClipData = intent.getClipData(); if (targetClipData == null && intent.getData() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java b/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java index 5a0dbb003..49be86ae0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java +++ b/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java @@ -18,159 +18,17 @@ package org.schabi.newpipe.util.urlfinder; -import androidx.annotation.RestrictTo; - import java.util.regex.Pattern; -import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; - /** * Commonly used regular expression patterns. */ public final class PatternsCompat { - /** - * Regular expression to match all IANA top-level domains. - * - * List accurate as of 2015/11/24. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py - */ - static final String IANA_TOP_LEVEL_DOMAINS = "(?:" - + "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active" - + "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica" - + "|amsterdam|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia" - + "|associates|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])" - + "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva" - + "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz" - + "|black|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots" - + "|boutique|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build" - + "|builders|business|buzz|bzh|b[abdefghijmnorstvwyz])" - + "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards" - + "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center" - + "|ceo|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani" - + "|cisco|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed" - + "|coach|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec" - + "|condos|construction|consulting|contractors|cooking|cool|coop|corsica|country" - + "|coupons|courses|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc" - + "|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])" - + "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta" - + "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory" - + "|discount|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])" - + "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises" - + "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert" - + "|exposed|express|e[cegrstu])" - + "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm" - + "|fashion|feedback|ferrero|film|final|finance|financial|firmdale|fish|fishing|fit" - + "|fitness|flights|florist|flowers|flsmidth|fly|foo|football|forex|forsale|forum" - + "|foundation|frl|frogans|fund|furniture|futbol|fyi|f[ijkmor])" - + "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving" - + "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov" - + "|grainger|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru" - + "|g[abdefghilmnpqrstuwy])" - + "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey" - + "|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house" - + "|how|hsbc|hyundai|h[kmnrtu])" - + "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink" - + "|institute|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau" - + "|iwc|i[delmnoqrst])" - + "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])" - + "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto" - + "|k[eghimnprwyz])" - + "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease" - + "|leclerc|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde" - + "|link|live|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury" - + "|l[abcikrstuvy])" - + "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba" - + "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi" - + "|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov" - + "|movie|movistar|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])" - + "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk" - + "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])" - + "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka" - + "|otsuka|ovh|om)" - + "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography" - + "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation" - + "|plumbing|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties" - + "|property|protection|pub|p[aefghklmnrstwy])" - + "|(?:qpon|quebec|qa)" - + "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent" - + "|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip" - + "|rocher|rocks|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])" - + "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo" - + "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat" - + "|security|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles" - + "|site|ski|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space" - + "|spiegel|spreadbetting|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study" - + "|style|sucks|supplies|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems" - + "|s[abcdeghijklmnortuvxyz])" - + "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica" - + "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo" - + "|tools|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust" - + "|tui|t[cdfghjklmnortvwz])" - + "|(?:ubs|university|uno|uol|u[agksyz])" - + "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin" - + "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])" - + "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki" - + "|williamhill|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])" - + "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c" - + "|\u043c\u043a\u0434|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430" - + "|\u043e\u043d\u043b\u0430\u0439\u043d|\u043e\u0440\u0433|\u0440\u0443\u0441" - + "|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431|\u0443\u043a\u0440" - + "|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd" - + "|\u0627\u0631\u0627\u0645\u0643\u0648|\u0627\u0644\u0627\u0631\u062f\u0646" - + "|\u0627\u0644\u062c\u0632\u0627\u0626\u0631" - + "|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629" - + "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a" - + "|\u0627\u06cc\u0631\u0627\u0646|\u0628\u0627\u0632\u0627\u0631" - + "|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633" - + "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629" - + "|\u0634\u0628\u0643\u0629|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646" - + "|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0643\u0648\u0645" - + "|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627|\u0645\u0648\u0642\u0639" - + "|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924" - + "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24" - + "|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe" - + "|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8" - + "|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd" - + "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21" - + "|\u0e44\u0e17\u0e22|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb" - + "|\u30b3\u30e0|\u4e16\u754c|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51" - + "|\u4f01\u4e1a|\u4f5b\u5c71|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8" - + "|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063|\u5546\u57ce|\u5546\u5e97|\u5546\u6807" - + "|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c|\u5e7f\u4e1c|\u6148\u5584" - + "|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c|\u65b0\u52a0\u5761" - + "|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f|\u70b9\u770b" - + "|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc" - + "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137" - + "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox|xerox|xin|xn\\-\\-11b4c3d" - + "|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g|xn\\-\\-3e0b707e" - + "|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim" - + "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks" - + "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais" - + "|xn\\-\\-9dbq2a|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g" - + "|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-czr694b|xn\\-\\-czrs0t" - + "|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h|xn\\-\\-estv75g" - + "|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s" - + "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c" - + "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i" - + "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d" - + "|xn\\-\\-kpry57d|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf" - + "|xn\\-\\-mgba3a3ejt|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd" - + "|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar" - + "|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m" - + "|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema|xn\\-\\-nyqy26a" - + "|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh" - + "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g" - + "|xn\\-\\-s9brj9c|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y" - + "|xn\\-\\-vermgensberater\\-ctb|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv" - + "|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a" - + "|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx" - + "|xn\\-\\-zfr164b|xperia|xxx|xyz)" - + "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])" - + "|(?:zara|zip|zone|zuerich|z[amw]))"; + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // CHANGED: Removed unused code // + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - public static final Pattern IP_ADDRESS - = Pattern.compile( + public static final Pattern IP_ADDRESS = Pattern.compile( "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" @@ -204,28 +62,11 @@ public final class PatternsCompat { */ private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR; - /** - * Valid characters for IRI TLD defined in RFC 3987. - */ - private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR; - /** * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets. */ - private static final String IRI_LABEL - = "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"; - - /** - * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters. - */ - private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w"; - - private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" + ")"; - - private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD; - - public static final Pattern DOMAIN_NAME - = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")"); + private static final String IRI_LABEL = + "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"; //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // CHANGED: Removed rtsp from supported protocols // @@ -245,59 +86,11 @@ public final class PatternsCompat { + ";/\\?:@&=#~" // plus optional query params + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*"; - /** - * Regular expression pattern to match most part of RFC 3987 - * Internationalized URLs, aka IRIs. - */ - public static final Pattern WEB_URL = Pattern.compile("(" - + "(" - + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?" - + "(?:" + DOMAIN_NAME + ")" - + "(?:" + PORT_NUMBER + ")?" - + ")" - + "(" + PATH_AND_QUERY + ")?" - + WORD_BOUNDARY - + ")"); - - /** - * Regular expression that matches known TLDs and punycode TLDs. - */ - private static final String STRICT_TLD = "(?:" - + IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")"; - - /** - * Regular expression that matches host names using {@link #STRICT_TLD}. - */ - private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+" - + STRICT_TLD + ")"; - - /** - * Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or - * {@link #IP_ADDRESS}. - */ - private static final Pattern STRICT_DOMAIN_NAME - = Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")"); - /** * Regular expression that matches domain names without a TLD. */ - private static final String RELAXED_DOMAIN_NAME - = "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" + "?)+" + "|" + IP_ADDRESS + ")"; - - /** - * Regular expression to match strings that do not start with a supported protocol. The TLDs - * are expected to be one of the known TLDs. - */ - private static final String WEB_URL_WITHOUT_PROTOCOL = "(" - + WORD_BOUNDARY - + "(? 0) { height = (int) (width / videoAspectRatio); } else { diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java index 798d08c72..d4fafc31a 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java @@ -17,10 +17,8 @@ */ package org.schabi.newpipe.views; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Rect; -import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -29,6 +27,7 @@ import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.WindowInsetsCompat; import org.schabi.newpipe.R; @@ -74,7 +73,6 @@ public final class FocusAwareCoordinator extends CoordinatorLayout { * Makes possible for multiple fragments to co-exist. Without this code * the first ViewGroup who consumes will be the last who receive the insets */ - @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public WindowInsets dispatchApplyWindowInsets(final WindowInsets insets) { boolean consumed = false; @@ -86,23 +84,22 @@ public final class FocusAwareCoordinator extends CoordinatorLayout { } } - if (consumed) { - insets.consumeSystemWindowInsets(); - } - return insets; + return consumed ? WindowInsetsCompat.CONSUMED.toWindowInsets() : insets; } /** - * Adjusts player's controls manually because fitsSystemWindows doesn't work when multiple + * Adjusts player's controls manually because onApplyWindowInsets doesn't work when multiple * receivers adjust its bounds. So when two listeners are present (like in profile page) * the player's controls will not receive insets. This method fixes it */ @Override - protected boolean fitSystemWindows(final Rect insets) { + public WindowInsets onApplyWindowInsets(final WindowInsets windowInsets) { + final var windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowInsets, this); + final var insets = windowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars()); final ViewGroup controls = findViewById(R.id.playbackControlRoot); if (controls != null) { controls.setPadding(insets.left, insets.top, insets.right, insets.bottom); } - return super.fitSystemWindows(insets); + return super.onApplyWindowInsets(windowInsets); } } diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java index 500562668..5c694c3a9 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java @@ -57,8 +57,8 @@ public final class FocusAwareDrawerLayout extends DrawerLayout { for (int i = 0; i < getChildCount(); ++i) { final View child = getChildAt(i); - final DrawerLayout.LayoutParams lp - = (DrawerLayout.LayoutParams) child.getLayoutParams(); + final DrawerLayout.LayoutParams lp = + (DrawerLayout.LayoutParams) child.getLayoutParams(); if (lp.gravity != 0 && isDrawerVisible(child)) { hasOpenPanels = true; @@ -85,8 +85,8 @@ public final class FocusAwareDrawerLayout extends DrawerLayout { for (int i = 0; i < getChildCount(); ++i) { final View child = getChildAt(i); - final DrawerLayout.LayoutParams lp - = (DrawerLayout.LayoutParams) child.getLayoutParams(); + final DrawerLayout.LayoutParams lp = + (DrawerLayout.LayoutParams) child.getLayoutParams(); if (lp.gravity == 0) { content = child; diff --git a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt index 649b60494..d0782e1a1 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/PlayerFastSeekOverlay.kt @@ -12,8 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START import androidx.constraintlayout.widget.ConstraintSet import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R -import org.schabi.newpipe.player.event.DisplayPortion -import org.schabi.newpipe.player.event.DoubleTapListener +import org.schabi.newpipe.player.gesture.DisplayPortion +import org.schabi.newpipe.player.gesture.DoubleTapListener class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs), DoubleTapListener { @@ -38,14 +38,14 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : private var performListener: PerformListener? = null - fun performListener(listener: PerformListener) = apply { + fun performListener(listener: PerformListener?) = apply { performListener = listener } private var seekSecondsSupplier: () -> Int = { 0 } - fun seekSecondsSupplier(supplier: () -> Int) = apply { - seekSecondsSupplier = supplier + fun seekSecondsSupplier(supplier: (() -> Int)?) = apply { + seekSecondsSupplier = supplier ?: { 0 } } // Indicates whether this (double) tap is the first of a series diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 9d8eaf9a5..04930b002 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -1,6 +1,5 @@ package us.shandian.giga.get; -import android.os.Build; import android.os.Handler; import android.system.ErrnoException; import android.system.OsConstants; @@ -316,16 +315,14 @@ public class DownloadMission extends Mission { public synchronized void notifyError(int code, Exception err) { Log.e(TAG, "notifyError() code = " + code, err); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (err != null && err.getCause() instanceof ErrnoException) { - int errno = ((ErrnoException) err.getCause()).errno; - if (errno == OsConstants.ENOSPC) { - code = ERROR_INSUFFICIENT_STORAGE; - err = null; - } else if (errno == OsConstants.EACCES) { - code = ERROR_PERMISSION_DENIED; - err = null; - } + if (err != null && err.getCause() instanceof ErrnoException) { + int errno = ((ErrnoException) err.getCause()).errno; + if (errno == OsConstants.ENOSPC) { + code = ERROR_INSUFFICIENT_STORAGE; + err = null; + } else if (errno == OsConstants.EACCES) { + code = ERROR_PERMISSION_DENIED; + err = null; } } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index d96b4fc5b..8b8a6ff09 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -4,10 +4,8 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.graphics.Bitmap; @@ -18,7 +16,6 @@ import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; -import android.os.Build; import android.os.Handler; import android.os.Handler.Callback; import android.os.IBinder; @@ -100,7 +97,6 @@ public class DownloadManagerService extends Service { private final ArrayList mEchoObservers = new ArrayList<>(1); private ConnectivityManager mConnectivityManager; - private BroadcastReceiver mNetworkStateListener = null; private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null; private SharedPreferences mPrefs = null; @@ -166,28 +162,18 @@ public class DownloadManagerService extends Service { mConnectivityManager = ContextCompat.getSystemService(this, ConnectivityManager.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(Network network) { - handleConnectivityState(false); - } + mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + handleConnectivityState(false); + } - @Override - public void onLost(Network network) { - handleConnectivityState(false); - } - }; - mConnectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkStateListenerL); - } else { - mNetworkStateListener = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - handleConnectivityState(false); - } - }; - registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } + @Override + public void onLost(Network network) { + handleConnectivityState(false); + } + }; + mConnectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkStateListenerL); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); @@ -246,10 +232,7 @@ public class DownloadManagerService extends Service { manageLock(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); - else - unregisterReceiver(mNetworkStateListener); + mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); @@ -263,21 +246,6 @@ public class DownloadManagerService extends Service { @Override public IBinder onBind(Intent intent) { - /* - int permissionCheck; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); - if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { - Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); - } - } - - permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { - Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); - } - */ - return mBinder; } @@ -473,12 +441,7 @@ public class DownloadManagerService extends Service { if (downloadDoneCount == 1) { downloadDoneList.append(name); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - downloadDoneNotification.setContentTitle(getString(R.string.app_name)); - } else { - downloadDoneNotification.setContentTitle(null); - } - + downloadDoneNotification.setContentTitle(null); downloadDoneNotification.setContentText(Localization.downloadCount(this, downloadDoneCount)); downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle() .setBigContentTitle(Localization.downloadCount(this, downloadDoneCount)) @@ -511,16 +474,10 @@ public class DownloadManagerService extends Service { .setContentIntent(mOpenDownloadList); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - downloadFailedNotification.setContentTitle(getString(R.string.app_name)); - downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName()))); - } else { - downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); - downloadFailedNotification.setContentText(mission.storage.getName()); - downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(mission.storage.getName())); - } + downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); + downloadFailedNotification.setContentText(mission.storage.getName()); + downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() + .bigText(mission.storage.getName())); mNotificationManager.notify(id, downloadFailedNotification.build()); } @@ -556,12 +513,7 @@ public class DownloadManagerService extends Service { if (path.charAt(0) == File.separatorChar) { Log.i(TAG, "Old save path style present: " + path); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - path = Uri.fromFile(new File(path)).toString(); - else - path = ""; - + path = ""; mPrefs.edit().putString(getString(prefKey), "").apply(); } diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 961c45bc5..343b13ef8 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -350,10 +350,8 @@ public class MissionAdapter extends Adapter implements Handler.Callb Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(resolveShareableUri(mission), mimeType); intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); - } if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { intent.addFlags(FLAG_ACTIVITY_NEW_TASK); } diff --git a/app/src/main/res/drawable-nodpi/buddy.png b/app/src/main/res/drawable-nodpi/buddy.png deleted file mode 100644 index 8713ee02b..000000000 Binary files a/app/src/main/res/drawable-nodpi/buddy.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/buddy_channel_item.png b/app/src/main/res/drawable-nodpi/buddy_channel_item.png deleted file mode 100644 index 64d4cb1a0..000000000 Binary files a/app/src/main/res/drawable-nodpi/buddy_channel_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/dummy_thumbnail.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail.png deleted file mode 100644 index 86f454186..000000000 Binary files a/app/src/main/res/drawable-nodpi/dummy_thumbnail.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png deleted file mode 100644 index 02f698918..000000000 Binary files a/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png deleted file mode 100644 index 9ba84fdb4..000000000 Binary files a/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/channel_banner.png b/app/src/main/res/drawable-nodpi/placeholder_channel_banner.png similarity index 100% rename from app/src/main/res/drawable-nodpi/channel_banner.png rename to app/src/main/res/drawable-nodpi/placeholder_channel_banner.png diff --git a/app/src/main/res/drawable/ic_history_future.xml b/app/src/main/res/drawable/ic_history_future.xml new file mode 100644 index 000000000..db6f2acbf --- /dev/null +++ b/app/src/main/res/drawable/ic_history_future.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/placeholder_person.xml b/app/src/main/res/drawable/placeholder_person.xml new file mode 100644 index 000000000..2b3229e8f --- /dev/null +++ b/app/src/main/res/drawable/placeholder_person.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/placeholder_thumbnail_playlist.xml b/app/src/main/res/drawable/placeholder_thumbnail_playlist.xml new file mode 100644 index 000000000..de286d860 --- /dev/null +++ b/app/src/main/res/drawable/placeholder_thumbnail_playlist.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/placeholder_thumbnail_video.xml b/app/src/main/res/drawable/placeholder_thumbnail_video.xml new file mode 100644 index 000000000..0b262f923 --- /dev/null +++ b/app/src/main/res/drawable/placeholder_thumbnail_video.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index c2359552e..7d3b43ecc 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -138,8 +138,8 @@ android:clickable="true" android:focusable="true" android:scaleType="fitCenter" + android:src="@drawable/ic_pause" android:tint="?attr/colorAccent" - app:srcCompat="@drawable/ic_pause" tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 851085b5b..5904724ad 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -57,7 +57,7 @@ android:scaleType="fitCenter" tools:ignore="RtlHardcoded" tools:layout_height="200dp" - tools:src="@drawable/dummy_thumbnail" /> + tools:src="@drawable/placeholder_thumbnail_video" /> @@ -199,7 +199,7 @@ android:layout_gravity="top|end" android:layout_marginTop="11dp" android:layout_marginEnd="10dp" - app:srcCompat="@drawable/ic_expand_more" + android:src="@drawable/ic_expand_more" tools:ignore="ContentDescription" /> @@ -271,7 +271,7 @@ android:layout_width="@dimen/video_item_detail_uploader_image_size" android:layout_height="@dimen/video_item_detail_uploader_image_size" android:contentDescription="@string/detail_uploader_thumbnail_view_description" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" /> @@ -326,18 +326,6 @@ tools:text="Uploader" /> - @@ -369,7 +357,7 @@ android:layout_height="@dimen/video_item_detail_like_image_height" android:layout_below="@id/detail_view_count_view" android:contentDescription="@string/detail_likes_img_view_description" - app:srcCompat="@drawable/ic_thumb_up" /> + android:src="@drawable/ic_thumb_up" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> @@ -526,12 +514,12 @@ android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:contentDescription="@string/share" + android:drawableTop="@drawable/ic_share" android:focusable="true" android:gravity="center" android:paddingVertical="@dimen/detail_control_padding" android:text="@string/share" - android:textSize="@dimen/detail_control_text_size" - app:drawableTopCompat="@drawable/ic_share" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> - + android:textSize="@dimen/detail_control_text_size" /> @@ -657,7 +645,7 @@ android:paddingLeft="@dimen/video_item_search_padding" android:paddingRight="@dimen/video_item_search_padding" android:scaleType="fitCenter" - android:src="@drawable/dummy_thumbnail" /> + android:src="@drawable/placeholder_thumbnail_video" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 97ccd199e..01d842812 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -25,7 +25,7 @@ android:layout_gravity="center_horizontal" app:behavior_hideable="true" app:behavior_peekHeight="0dp" - app:layout_behavior="org.schabi.newpipe.player.event.CustomBottomSheetBehavior" /> + app:layout_behavior="org.schabi.newpipe.player.gesture.CustomBottomSheetBehavior" /> diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index 24e062932..29efa36f9 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -174,8 +174,8 @@ android:clickable="true" android:focusable="true" android:scaleType="fitXY" + android:src="@drawable/ic_repeat" android:tint="?attr/colorAccent" - app:srcCompat="@drawable/ic_repeat" tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/channel_header.xml b/app/src/main/res/layout/channel_header.xml index 86a308b9f..9d1304635 100644 --- a/app/src/main/res/layout/channel_header.xml +++ b/app/src/main/res/layout/channel_header.xml @@ -17,8 +17,8 @@ android:layout_height="70dp" android:background="@android:color/black" android:fitsSystemWindows="true" - android:scaleType="centerCrop" - android:src="@drawable/channel_banner" + android:scaleType="fitCenter" + android:src="@drawable/placeholder_channel_banner" tools:ignore="ContentDescription" /> @@ -44,7 +44,7 @@ android:layout_height="@dimen/sub_channel_avatar_size" android:layout_gravity="bottom|right" android:padding="1dp" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" android:visibility="gone" app:shapeAppearance="@style/CircularImageView" app:strokeColor="#ffffff" diff --git a/app/src/main/res/layout/dialog_feed_group_create.xml b/app/src/main/res/layout/dialog_feed_group_create.xml index a08041e97..464940238 100644 --- a/app/src/main/res/layout/dialog_feed_group_create.xml +++ b/app/src/main/res/layout/dialog_feed_group_create.xml @@ -193,8 +193,8 @@ android:layout_centerVertical="true" android:minWidth="0dp" android:scaleType="centerInside" + android:src="@drawable/ic_delete" android:visibility="gone" - app:srcCompat="@drawable/ic_delete" tools:ignore="ContentDescription" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml index e402f4fb1..cc506cc79 100644 --- a/app/src/main/res/layout/dialog_playback_parameter.xml +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -1,6 +1,5 @@ @@ -20,7 +19,7 @@ android:layout_centerVertical="true" android:layout_marginLeft="12dp" android:layout_marginRight="12dp" - app:srcCompat="@drawable/ic_playlist_add" + android:src="@drawable/ic_playlist_add" tools:ignore="ContentDescription,RtlHardcoded" /> + tools:src="@drawable/ic_smart_display" /> diff --git a/app/src/main/res/layout/feed_group_add_new_item.xml b/app/src/main/res/layout/feed_group_add_new_item.xml index df3fc9b3a..0dfe819a6 100644 --- a/app/src/main/res/layout/feed_group_add_new_item.xml +++ b/app/src/main/res/layout/feed_group_add_new_item.xml @@ -24,7 +24,7 @@ android:layout_height="14dp" android:layout_gravity="center" android:scaleType="centerInside" - app:srcCompat="@drawable/ic_add" + android:src="@drawable/ic_add" tools:ignore="ContentDescription" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index db93f1d8d..1807acb10 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -1,5 +1,4 @@ + android:src="@mipmap/ic_launcher" /> + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/fragment_description.xml b/app/src/main/res/layout/fragment_description.xml index b020d5db3..157b8f394 100644 --- a/app/src/main/res/layout/fragment_description.xml +++ b/app/src/main/res/layout/fragment_description.xml @@ -38,11 +38,11 @@ android:contentDescription="@string/description_select_enable" android:focusable="true" android:padding="5dp" + android:src="@drawable/ic_select_all" app:layout_constraintBottom_toTopOf="@+id/barrier" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_select_all" /> + app:layout_constraintTop_toTopOf="parent" /> @@ -57,7 +56,7 @@ android:layout_alignParentEnd="true" android:layout_marginStart="6dp" android:layout_marginEnd="12dp" - app:srcCompat="@drawable/ic_refresh" + android:src="@drawable/ic_refresh" tools:ignore="ContentDescription" /> + app:fabSize="auto" /> diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 08a9bcf0a..c794c5a55 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -47,7 +47,7 @@ android:scaleType="fitCenter" tools:ignore="RtlHardcoded" tools:layout_height="200dp" - tools:src="@drawable/dummy_thumbnail" /> + tools:src="@drawable/placeholder_thumbnail_video" /> @@ -187,7 +187,7 @@ android:layout_gravity="top|end" android:layout_marginTop="11dp" android:layout_marginEnd="10dp" - app:srcCompat="@drawable/ic_expand_more" + android:src="@drawable/ic_expand_more" tools:ignore="ContentDescription" /> @@ -258,7 +258,7 @@ android:layout_width="@dimen/video_item_detail_uploader_image_size" android:layout_height="@dimen/video_item_detail_uploader_image_size" android:contentDescription="@string/detail_uploader_thumbnail_view_description" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" /> - - @@ -357,7 +344,7 @@ android:layout_height="@dimen/video_item_detail_like_image_height" android:layout_below="@id/detail_view_count_view" android:contentDescription="@string/detail_likes_img_view_description" - app:srcCompat="@drawable/ic_thumb_up" /> + android:src="@drawable/ic_thumb_up" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> @@ -510,12 +497,12 @@ android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:contentDescription="@string/share" + android:drawableTop="@drawable/ic_share" android:focusable="true" android:gravity="center" android:paddingVertical="@dimen/detail_control_padding" android:text="@string/share" - android:textSize="@dimen/detail_control_text_size" - app:drawableTopCompat="@drawable/ic_share" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> + android:textSize="@dimen/detail_control_text_size" /> - + android:textSize="@dimen/detail_control_text_size" /> @@ -631,8 +618,7 @@ android:gravity="center_vertical" android:paddingLeft="@dimen/video_item_search_padding" android:paddingRight="@dimen/video_item_search_padding" - android:scaleType="fitCenter" - android:src="@drawable/dummy_thumbnail" /> + android:scaleType="fitCenter" /> diff --git a/app/src/main/res/layout/item_instance.xml b/app/src/main/res/layout/item_instance.xml index dd5b4156f..12ecb2ea7 100644 --- a/app/src/main/res/layout/item_instance.xml +++ b/app/src/main/res/layout/item_instance.xml @@ -75,7 +75,7 @@ android:paddingTop="12dp" android:paddingRight="10dp" android:paddingBottom="12dp" - app:srcCompat="@drawable/ic_drag_handle" + android:src="@drawable/ic_drag_handle" tools:ignore="ContentDescription,RtlHardcoded" /> diff --git a/app/src/main/res/layout/item_search_suggestion.xml b/app/src/main/res/layout/item_search_suggestion.xml index 4b1025fea..f7a07bbcc 100644 --- a/app/src/main/res/layout/item_search_suggestion.xml +++ b/app/src/main/res/layout/item_search_suggestion.xml @@ -1,6 +1,5 @@ diff --git a/app/src/main/res/layout/item_stream_segment.xml b/app/src/main/res/layout/item_stream_segment.xml index 3c7631788..e9bdfe3ac 100644 --- a/app/src/main/res/layout/item_stream_segment.xml +++ b/app/src/main/res/layout/item_stream_segment.xml @@ -17,8 +17,8 @@ android:id="@+id/previewImage" android:layout_width="0dp" android:layout_height="@dimen/play_queue_thumbnail_width" - android:scaleType="centerCrop" - android:src="@drawable/dummy_thumbnail" + android:scaleType="fitCenter" + android:src="@drawable/placeholder_thumbnail_video" app:layout_constraintDimensionRatio="16:9" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/list_channel_grid_item.xml b/app/src/main/res/layout/list_channel_grid_item.xml index d9084bbe9..3112a849f 100644 --- a/app/src/main/res/layout/list_channel_grid_item.xml +++ b/app/src/main/res/layout/list_channel_grid_item.xml @@ -18,7 +18,7 @@ android:layout_centerHorizontal="true" android:layout_margin="2dp" android:contentDescription="@string/detail_uploader_thumbnail_view_description" - android:src="@drawable/buddy_channel_item" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" tools:ignore="RtlHardcoded" /> diff --git a/app/src/main/res/layout/list_channel_item.xml b/app/src/main/res/layout/list_channel_item.xml index c09c17ead..cf4685e50 100644 --- a/app/src/main/res/layout/list_channel_item.xml +++ b/app/src/main/res/layout/list_channel_item.xml @@ -64,7 +64,7 @@ android:layout_height="@dimen/video_item_search_avatar_image_height" android:layout_marginLeft="@dimen/video_item_search_avatar_left_margin" android:layout_marginRight="@dimen/video_item_search_avatar_right_margin" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/itemTitleView" app:layout_constraintHorizontal_bias="0.5" diff --git a/app/src/main/res/layout/list_channel_mini_item.xml b/app/src/main/res/layout/list_channel_mini_item.xml index b66e07a12..473709bbd 100644 --- a/app/src/main/res/layout/list_channel_mini_item.xml +++ b/app/src/main/res/layout/list_channel_mini_item.xml @@ -17,7 +17,7 @@ android:layout_centerVertical="true" android:layout_marginStart="3dp" android:layout_marginRight="15dp" - android:src="@drawable/buddy_channel_item" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" tools:ignore="RtlHardcoded" /> diff --git a/app/src/main/res/layout/list_choose_tabs.xml b/app/src/main/res/layout/list_choose_tabs.xml index e6cebda11..ed7ad94c4 100644 --- a/app/src/main/res/layout/list_choose_tabs.xml +++ b/app/src/main/res/layout/list_choose_tabs.xml @@ -55,7 +55,7 @@ android:paddingTop="12dp" android:paddingRight="16dp" android:paddingBottom="12dp" - app:srcCompat="@drawable/ic_drag_handle" + android:src="@drawable/ic_drag_handle" tools:ignore="ContentDescription,RtlHardcoded" /> diff --git a/app/src/main/res/layout/list_comments_item.xml b/app/src/main/res/layout/list_comments_item.xml index 3b148de03..ad73c5ff4 100644 --- a/app/src/main/res/layout/list_comments_item.xml +++ b/app/src/main/res/layout/list_comments_item.xml @@ -20,7 +20,7 @@ android:layout_marginLeft="3dp" android:layout_marginRight="@dimen/comment_item_avatar_right_margin" android:focusable="false" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" tools:ignore="RtlHardcoded" /> @@ -32,8 +32,8 @@ android:layout_marginRight="@dimen/video_item_detail_pinned_right_margin" android:layout_toEndOf="@+id/itemThumbnailView" android:contentDescription="@string/detail_pinned_comment_view_description" + android:src="@drawable/ic_pin" android:visibility="gone" - app:srcCompat="@drawable/ic_pin" tools:visibility="visible" /> + android:src="@drawable/ic_thumb_up" /> - - - - - + tools:visibility="visible" /> @@ -39,7 +39,7 @@ android:layout_below="@id/itemCommentContentView" android:layout_toRightOf="@+id/itemThumbnailView" android:contentDescription="@string/detail_likes_img_view_description" - app:srcCompat="@drawable/ic_thumb_up" /> + android:src="@drawable/ic_thumb_up" /> - - - - diff --git a/app/src/main/res/layout/list_stream_item.xml b/app/src/main/res/layout/list_stream_item.xml index 5806ed96e..793942568 100644 --- a/app/src/main/res/layout/list_stream_item.xml +++ b/app/src/main/res/layout/list_stream_item.xml @@ -14,8 +14,8 @@ android:id="@+id/itemThumbnailView" android:layout_width="@dimen/video_item_search_thumbnail_image_width" android:layout_height="@dimen/video_item_search_thumbnail_image_height" - android:scaleType="centerCrop" - android:src="@drawable/dummy_thumbnail" + android:scaleType="fitCenter" + android:src="@drawable/placeholder_thumbnail_video" app:layout_constraintBottom_toTopOf="@+id/itemProgressView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/list_stream_mini_item.xml b/app/src/main/res/layout/list_stream_mini_item.xml index 5de3eac46..2eab66eaf 100644 --- a/app/src/main/res/layout/list_stream_mini_item.xml +++ b/app/src/main/res/layout/list_stream_mini_item.xml @@ -17,8 +17,8 @@ android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginRight="@dimen/video_item_search_image_right_margin" - android:scaleType="centerCrop" - android:src="@drawable/dummy_thumbnail" + android:scaleType="fitCenter" + android:src="@drawable/placeholder_thumbnail_video" tools:ignore="RtlHardcoded" /> diff --git a/app/src/main/res/layout/mission_item_linear.xml b/app/src/main/res/layout/mission_item_linear.xml index d7235fcaf..ce2d1af4b 100644 --- a/app/src/main/res/layout/mission_item_linear.xml +++ b/app/src/main/res/layout/mission_item_linear.xml @@ -72,7 +72,7 @@ android:layout_marginRight="4dp" android:contentDescription="TODO" android:scaleType="centerInside" - app:srcCompat="@drawable/ic_more_vert" + android:src="@drawable/ic_more_vert" app:tint="?attr/actionColor" /> diff --git a/app/src/main/res/layout/picker_subscription_item.xml b/app/src/main/res/layout/picker_subscription_item.xml index 1aaa1e7d8..6ebc10051 100644 --- a/app/src/main/res/layout/picker_subscription_item.xml +++ b/app/src/main/res/layout/picker_subscription_item.xml @@ -22,7 +22,7 @@ android:layout_width="48dp" android:layout_height="48dp" app:shapeAppearance="@style/CircularImageView" - tools:src="@drawable/buddy_channel_item" /> + tools:src="@drawable/placeholder_person" /> diff --git a/app/src/main/res/layout/play_queue_item.xml b/app/src/main/res/layout/play_queue_item.xml index f266af091..2ad9d3e89 100644 --- a/app/src/main/res/layout/play_queue_item.xml +++ b/app/src/main/res/layout/play_queue_item.xml @@ -17,8 +17,8 @@ android:layout_marginStart="@dimen/video_item_search_image_right_margin" android:layout_marginTop="@dimen/video_item_search_image_right_margin" android:layout_marginBottom="@dimen/video_item_search_image_right_margin" - android:scaleType="centerCrop" - android:src="@drawable/dummy_thumbnail" + android:scaleType="fitCenter" + android:src="@drawable/placeholder_thumbnail_video" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -89,10 +89,10 @@ android:layout_gravity="center_vertical" android:paddingHorizontal="@dimen/video_item_search_image_right_margin" android:scaleType="center" + android:src="@drawable/ic_drag_handle" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_drag_handle" tools:ignore="ContentDescription,RtlHardcoded" /> diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index d748942e0..60cbcf7c4 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -112,8 +112,8 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitXY" + android:src="@drawable/ic_close" android:visibility="gone" - app:srcCompat="@drawable/ic_close" app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" /> @@ -198,8 +198,8 @@ android:paddingEnd="3dp" android:paddingBottom="3dp" android:scaleType="fitCenter" + android:src="@drawable/ic_list" android:visibility="gone" - app:srcCompat="@drawable/ic_list" app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" /> @@ -216,8 +216,8 @@ android:paddingEnd="6dp" android:paddingBottom="3dp" android:scaleType="fitCenter" + android:src="@drawable/ic_format_list_numbered" android:visibility="gone" - app:srcCompat="@drawable/ic_format_list_numbered" app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" /> @@ -230,7 +230,7 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitXY" - app:srcCompat="@drawable/ic_expand_more" + android:src="@drawable/ic_expand_more" app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" /> @@ -294,7 +294,7 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitXY" - app:srcCompat="@drawable/ic_cast" + android:src="@drawable/ic_cast" app:tint="@color/white" tools:ignore="RtlHardcoded" /> @@ -309,7 +309,7 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitXY" - app:srcCompat="@drawable/ic_language" + android:src="@drawable/ic_language" app:tint="@color/white" tools:ignore="RtlHardcoded" /> @@ -324,7 +324,7 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitXY" - app:srcCompat="@drawable/ic_share" + android:src="@drawable/ic_share" app:tint="@color/white" tools:ignore="RtlHardcoded" /> @@ -338,7 +338,7 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitXY" - app:srcCompat="@drawable/ic_volume_off" + android:src="@drawable/ic_volume_off" app:tint="@color/white" tools:ignore="RtlHardcoded" /> @@ -351,8 +351,8 @@ android:focusable="true" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitCenter" + android:src="@drawable/ic_fullscreen" android:visibility="gone" - app:srcCompat="@drawable/ic_fullscreen" app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" tools:visibility="visible" /> @@ -397,8 +397,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="2dp" + android:src="@drawable/placeholder_thumbnail_video" android:visibility="gone" - app:srcCompat="@drawable/dummy_thumbnail" tools:visibility="visible" /> @@ -475,8 +475,8 @@ android:nextFocusUp="@id/playbackSeekBar" android:padding="@dimen/player_main_buttons_padding" android:scaleType="fitCenter" + android:src="@drawable/ic_fullscreen" android:visibility="gone" - app:srcCompat="@drawable/ic_fullscreen" app:tint="@color/white" tools:ignore="ContentDescription,RtlHardcoded" tools:visibility="visible" /> @@ -500,7 +500,7 @@ android:clickable="true" android:focusable="true" android:scaleType="fitCenter" - app:srcCompat="@drawable/ic_previous" + android:src="@drawable/ic_previous" app:tint="@color/white" tools:ignore="ContentDescription" /> @@ -512,7 +512,7 @@ android:layout_weight="1" android:background="?attr/selectableItemBackgroundBorderless" android:scaleType="fitCenter" - app:srcCompat="@drawable/ic_pause" + android:src="@drawable/ic_pause" app:tint="@color/white" tools:ignore="ContentDescription" /> @@ -526,7 +526,7 @@ android:clickable="true" android:focusable="true" android:scaleType="fitCenter" - app:srcCompat="@drawable/ic_next" + android:src="@drawable/ic_next" app:tint="@color/white" tools:ignore="ContentDescription" /> @@ -577,8 +577,8 @@ android:focusable="true" android:padding="10dp" android:scaleType="fitXY" + android:src="@drawable/exo_controls_repeat_off" android:tint="?attr/colorAccent" - app:srcCompat="@drawable/exo_controls_repeat_off" tools:ignore="ContentDescription,RtlHardcoded" /> diff --git a/app/src/main/res/layout/player_fast_seek_seconds_view.xml b/app/src/main/res/layout/player_fast_seek_seconds_view.xml index 57f5aa787..2946f8449 100644 --- a/app/src/main/res/layout/player_fast_seek_seconds_view.xml +++ b/app/src/main/res/layout/player_fast_seek_seconds_view.xml @@ -1,6 +1,5 @@ diff --git a/app/src/main/res/layout/player_popup_close_overlay.xml b/app/src/main/res/layout/player_popup_close_overlay.xml index b2403583d..10d81d77e 100644 --- a/app/src/main/res/layout/player_popup_close_overlay.xml +++ b/app/src/main/res/layout/player_popup_close_overlay.xml @@ -10,8 +10,8 @@ android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:layout_marginBottom="24dp" + android:src="@drawable/ic_close" app:backgroundTint="@color/light_youtube_primary_color" app:borderWidth="0dp" - app:fabSize="normal" - app:srcCompat="@drawable/ic_close" /> + app:fabSize="normal" /> diff --git a/app/src/main/res/layout/playlist_control.xml b/app/src/main/res/layout/playlist_control.xml index a5f258f50..5a8856128 100644 --- a/app/src/main/res/layout/playlist_control.xml +++ b/app/src/main/res/layout/playlist_control.xml @@ -1,6 +1,5 @@ + android:textSize="@dimen/channel_rss_title_size" /> + android:textSize="@dimen/channel_rss_title_size" /> diff --git a/app/src/main/res/layout/playlist_header.xml b/app/src/main/res/layout/playlist_header.xml index 7c85a6d0d..9c038db3a 100644 --- a/app/src/main/res/layout/playlist_header.xml +++ b/app/src/main/res/layout/playlist_header.xml @@ -45,7 +45,7 @@ android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:padding="0.7dp" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" app:strokeColor="#ffffff" app:strokeWidth="1dp" /> diff --git a/app/src/main/res/layout/select_channel_item.xml b/app/src/main/res/layout/select_channel_item.xml index cfaaf2760..c5fd51bb8 100644 --- a/app/src/main/res/layout/select_channel_item.xml +++ b/app/src/main/res/layout/select_channel_item.xml @@ -19,7 +19,7 @@ android:layout_alignParentTop="true" android:layout_marginStart="3dp" android:layout_marginRight="8dp" - android:src="@drawable/buddy" + android:src="@drawable/placeholder_person" app:shapeAppearance="@style/CircularImageView" tools:ignore="RtlHardcoded" /> diff --git a/app/src/main/res/layout/statistic_playlist_control.xml b/app/src/main/res/layout/statistic_playlist_control.xml index 577e94e4b..36540d32e 100644 --- a/app/src/main/res/layout/statistic_playlist_control.xml +++ b/app/src/main/res/layout/statistic_playlist_control.xml @@ -1,6 +1,5 @@ @@ -13,7 +12,7 @@ android:layout_centerVertical="true" android:layout_marginLeft="6dp" android:scaleType="fitCenter" - app:srcCompat="@drawable/ic_volume_off" + android:src="@drawable/ic_volume_off" tools:ignore="ContentDescription,RtlHardcoded" /> diff --git a/app/src/main/res/menu/menu_feed_fragment.xml b/app/src/main/res/menu/menu_feed_fragment.xml index 7a948ea8a..775a2d979 100644 --- a/app/src/main/res/menu/menu_feed_fragment.xml +++ b/app/src/main/res/menu/menu_feed_fragment.xml @@ -11,10 +11,19 @@ android:title="@string/feed_toggle_show_played_items" app:showAsAction="ifRoom" /> + + diff --git a/app/src/main/res/menu/menu_local_playlist.xml b/app/src/main/res/menu/menu_local_playlist.xml index 41791b791..0ff182b48 100644 --- a/app/src/main/res/menu/menu_local_playlist.xml +++ b/app/src/main/res/menu/menu_local_playlist.xml @@ -2,6 +2,12 @@ + + © %1$sبواسطة%2$sتحت%3$s صفحة الكشك حدد كشك - ابدأ التشغيل في الخلفية + بدأ التشغيل في الخلفية المحتوى الإفتراضي حسب البلد الإنتقال إلى التشغيل في الخلفية الإنتقال إلى التشغيل في النافذة المنبثقة @@ -250,7 +250,7 @@ لا يمكن أن يكون اسم الملف فارغًا حدث خطأٌ ما: %1$s ملف مضغوط ZIP غير صالح - إزالة الفواصل المرجعية + إزالة الإشارة المرجعية تناسب مع الشاشة توليد تلقائي إستيراد @@ -600,8 +600,8 @@ زر الإجراء الثالث زر الإجراء الثاني زر الإجراء الأول - قياس الصورة المصغرة للفيديو المعروض في الإشعار من 16:9 إلى 1:1 نسبة العرض إلى الارتفاع (قد يؤدي إلى تشوهات) - مقياس الصورة المصغرة إلى نسبة عرض إلى ارتفاع 1:1 + قم بقص الصورة المصغرة للفيديو الموضحة في الإشعار من نسبة العرض إلى الارتفاع 16: 9 إلى 1: 1 + اقتصاص الصورة المصغرة إلى نسبة العرض إلى الارتفاع 1:1 امسح ملفات تعريف الارتباط التي يخزنها NewPipe عند حل reCAPTCHA تم مسح ملفات تعريف الارتباط reCAPTCHA امسح ملفات تعريف الارتباط reCAPTCHA @@ -673,7 +673,6 @@ تعذر تحميل تغذية لـ\'%s\'. خطأ في تحميل الخلاصة بدءًا من Android 10، يتم دعم \"Storage Access Framework\" فقط - \"Storage Access Framework\" غير مدعوم على Android KitKat والإصدارات الأقدم سيتم سؤالك عن مكان حفظ كل تنزيل لم يتم تعيين مجلد التحميل، الرجاء اختيار مجلد التحميل الافتراضي الآن إيقاف @@ -685,7 +684,7 @@ جودة منخفضة (أصغر) جودة عالية (أكبر) معاينة مصغرة على شريط التمرير - علّمه كفيديو تمت مشاهدته + تعليم كفيديو تمت مشاهدته أُعجب بها منشئ المحتوى أظهر أشرطة ملونة لبيكاسو أعلى الصور تشير إلى مصدرها: الأحمر للشبكة والأزرق للقرص والأخضر للذاكرة إظهار مؤشرات الصور @@ -726,14 +725,14 @@ حدث خطأ، انظر للإشعار قم بإنشاء تنبيه بالخطأ لم يتم العثور على مدير ملفات مناسب لهذا الإجراء. -\nالرجاء تثبيت مدير ملفات أو محاولة تعطيل \"%s\" في إعدادات التنزيل. +\nيرجى تثبيت مدير ملفات أو محاولة تعطيل \"%s\" في إعدادات التنزيل إظهار خطأ snackbar لم يتم العثور على مدير ملفات مناسب لهذا الإجراء. -\nالرجاء تثبيت مدير ملفات متوافق مع Storage Access Framework. +\nالرجاء تثبيت مدير ملفات متوافق مع إطار عمل الوصول إلى التخزين تعليق مثبت LeakCanary غير متوفر الافتراضي ExoPlayer - تغيير حجم الفاصل الزمني للتحميل (حاليا %s). قد تؤدي القيمة الأقل إلى تسريع تحميل الفيديو الأولي. تتطلب التغييرات إعادة تشغيل المشغل. + تغيير حجم الفاصل الزمني للتحميل (حاليا %s). قد تؤدي القيمة الأقل إلى تسريع تحميل الفيديو الأولي. تتطلب التغييرات إعادة تشغيل المشغل تكوين إشعار مشغل البث الحالي الإشعارات تحميل تفاصيل البث… @@ -770,4 +769,5 @@ تنسيق غير معروف جودة غير معروفة حجم الفاصل الزمني لتحميل التشغيل + عرض مقاطع الفيديو المستقبلية \ No newline at end of file diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 95b7b76f4..057be6d02 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -405,7 +405,7 @@ Sükut zamanı sürətlə irəlilə Yeni yayım bildirişləri Abunəliklərdən yeni yayımlar haqqında bildiriş göndər - Tezliyin yoxlanılması + Yoxlama tezliyi Tələb olunan şəbəkə bağlantısı İstənilən şəbəkə Tətbiq keçidində kiçildin @@ -463,7 +463,6 @@ Server məlumat göndərmir Bu endirməni bərpa etmək mümkün deyil Sizdən hər endirmənin harada saxlanacağı soruşulacaq - \'Yaddaş Giriş Çərçivəsi\' Android KitKat və ondan aşağı versiyalarda dəstəklənmir \"Yaddaş Giriş Çərçivəsi\"yalnız Android 10\'dan başlayaraq dəstəklənir Kanalın avatar miniatürü Sevdiyiniz gecə mövzusunu aşağıda seçə bilərsiniz @@ -497,8 +496,8 @@ Video oynadıcı Video fayl xülasəsi prosesi üçün bildirişlər Açın - Kiçik şəkili 1:1 aspekt nisbətinə ölçün - Yükləmə intervalının həcmini dəyişdirin (hazırda %s). Daha aşağı dəyər ilkin video yükləməni sürətləndirə bilər. Dəyişikliklər oynadıcının yenidən başladılmasını tələb edir. + Miniatürü 1:1 aspekt nisbətinə kəsin + Yükləmə intervalının həcmini dəyişdirin (hazırda %s). Daha aşağı dəyər ilkin video yükləməni sürətləndirə bilər. Dəyişikliklər oynadıcının yenidən başladılmasını tələb edir Yayım yaradıcısı, məzmunu və ya axtarış sorğusu haqqında əlavə məlumat olan üst məlumat qutularını gizlətmək üçün söndürün Əlaqədar yayımı əlavə etməklə (təkrar etməyən) sonlanacaq oynatma sırasını davam etdir Kənar axtarış təklifləri @@ -701,13 +700,13 @@ ExoPlayer məhdudiyyətlərinə görə axtarış müddəti %d saniyəyə təyin edildi 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) Bu əməliyyat üçün uyğun fayl meneceri tapılmadı. -\nZəhmət olmasa, fayl menecerini quraşdırın və ya endirmə tənzimləmələrində \'%s\'-i deaktiv etməyə çalışın. +\nZəhmət olmasa, fayl menecerini quraşdırın və ya endirmə tənzimləmələrində \'%s\'-i deaktiv etməyə çalışın \'%s\' üçün axın yükləmək mümkün olmadı. Bu əməliyyat üçün uyğun fayl meneceri tapılmadı. -\nZəhmət olmasa ,Yaddaş Giriş Çərçivəsinə uyğun fayl menecerini quraşdırın. +\nZəhmət olmasa ,Yaddaş Giriş Çərçivəsinə uyğun fayl menecerini quraşdırın 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. İndi açıqlamadakı mətni seçə bilərsiniz. Nəzərə alın ki, seçim rejimində səhifə titrəyə bilər və keçidlər kliklənməyə bilər. - Bildirişdə göstərilən video miniatürünü 16:9-dan 1:1 nisbətinə qədər ölçün (pozuntulara səbəb ola bilər) + Bildirişdə göstərilən video miniatürünü 16:9-dan 1:1 nisbətinə qədər kəsin Aşağıdakı bildiriş fəaliyyətini hər birinin üzərinə toxunaraq redaktə edin. Sağdakı təsdiq qutularından istifadə edərək yığcam bildirişdə göstərilməsi üçün onlardan üçə qədərini seçin Belə fayl/məzmun mənbəyi yoxdur Seçilmiş yayım xarici oynadıcılar tərəfindən dəstəklənmir @@ -718,4 +717,5 @@ Naməlum format Naməlum keyfiyyət Oynatma yükləmə intervalı həcmi + Gələcək videoları göstərin \ No newline at end of file diff --git a/app/src/main/res/values-b+zh+HANS+CN/strings.xml b/app/src/main/res/values-b+zh+HANS+CN/strings.xml deleted file mode 100644 index b3642829c..000000000 --- a/app/src/main/res/values-b+zh+HANS+CN/strings.xml +++ /dev/null @@ -1,708 +0,0 @@ - - - 点击放大镜图标即可开始使用。 - 发布于 %1$s - 在浏览器中打开 - 在悬浮窗中打开 - 您要找的是不是“%1$s”? - 找不到串流播放器(您可以安装 VLC 以播放串流)。 - 下载串流文件 - 安装 - 取消 - 分享 - 下载 - 搜索 - 设置 - 分享给 - 视频下载路径 - 已下载的视频将存储于此 - 请选择下载视频的保存位置 - 已下载的音频将存储于此 - 选择下载音频的储存位置 - 使用 Kodi 播放 - 主题 - 浅色 - 深色 - 暗黑 - 下载 - 不支持的 URL - 外观 - 全部 - 网络错误 - - %s 个视频 - - 已停用 - 后台播放 - 显示搜索建议 - 订阅 - 已订阅 - 记录播放记录 - 播放器 - 历史记录与缓存 - 撤销 - 全部播放 - 总是 - 仅一次 - 添加至 - 文件 - 加载封面 - 清空播放历史 - - 最小化至后台播放 - 最小化至悬浮窗播放 - 频道 - 播放列表 - 取消订阅 - 更新 - 文件已删除 - 无法得知订阅人数 - 有新版本时,显示通知提示更新应用 - 网格 - NewPipe 可更新! - 服务器不接受多线程下载, 使用 @string/msg_threads = 1 重试 - 自动播放 - 清空数据 - 播放历史已删除 - 喜欢 - 不喜欢 - 视频 - 音频 - 重试 - - %s 次观看 - - - 百万 - 开始 - 暂停 - 删除 - 校验 - 确定 - 文件名 - 线程数 - 错误 - 点击了解详情 - 请稍候… - 已复制到剪贴板 - 悬浮窗播放 - 关于 NewPipe - 第三方许可 - © %1$s 由 %2$s 遵循 %3$s 协议发布 - 关于 - 许可证 - 下载 - 文件名中允许的字符 - 无效字符将会被替换为该字符 - 字母和数字 - 特殊字符 - 没有结果 - 无人订阅 - - %s 位订阅者 - - 无视频 - 拖动以重新排序 - 新建 - 退出 - 重命名 - 未安装可播放此文件的应用程序 - 已删除一个项目。 - 自定义主页显示的标签页 - 列表视图模式 - 已完成 - 等待中 - 已暂停 - 已加入队列 - 加入队列 - 操作已被系统拒绝 - 下载失败 - 没有评论 - 切换服务,当前选择: - 找不到串流播放器。是否安装 VLC? - 使用外部视频播放器 - 使用外部音频播放器 - 音频下载文件夹 - 默认分辨率 - 未找到 Kore。是否安装 Kore? - 显示“使用 Kodi 播放”选项 - 显示“通过 Kodi 媒体中心播放视频”选项 - 音频 - 默认音频格式 - 显示“接下来”和“相似视频” - 视频和音频 - 后台播放 - 内容 - 显示年龄限制的内容 - 直播 - 下载 - 下载 - 反馈错误 - 错误 - 无法加载所有缩略图 - 无法解密视频的 URL 签名 - 无法解析网址 - 内容不可用 - 无法设置下载菜单 - App/UI 崩溃 - 抱歉, 这本不该发生。 - 使用电子邮件反馈错误 - 抱歉,发生了一些错误。 - 反馈 - 信息: - 发生了什么: - 详情:\\n请求:\\n内容语言:\\n内容国家:\\n客户端语言:\\n服务:\\nGMT时间:\\n包名:\\n版本:\\n操作系统版本: - 您的附加说明(请用英文): - 详细信息: - 播放视频,时长: - 视频上传者的头像缩略图 - 十亿 - NewPipe 正在下载 - 请稍后在设置中设定下载目录 - 使用悬浮窗模式 -\n需要该权限 - reCAPTCHA 验证 - 已请求新的 reCAPTCHA 验证 - 在悬浮窗中播放 - 默认分辨率(悬浮窗模式) - 使用更高的分辨率 - 仅部分设备支持播放 2K 或 4K 视频 - 清除 - 记住悬浮窗属性(大小与位置) - 记住上一次使用悬浮窗的大小和位置 - 部分分辨率下没有音频 - 选择搜索时显示的建议 - 最佳分辨率 - 自由且小巧的 Android 媒体播放器。 - 在 GitHub 上查看 - NewPipe 的许可证 - 你是否想过要翻译、设计、清理或重构代码——我们始终欢迎你来贡献! - 阅读许可证 - 贡献 - 替换字符 - 已取消订阅频道 - 无法修改订阅 - 无法更新订阅 - 订阅 - 最新 - 自动恢复播放 - 在播放被打断(例如突然来电)后恢复播放 - 记录搜索历史 - Newpipe 将在本地存储搜索历史记录 - Newpipe 将保留播放记录 - 历史记录 - 历史记录 - NewPipe 通知 - NewPipe 播放器的通知 - 默认视频格式 - 行为 - 空空如也 - 无人观看过 - 是否删除此条搜索历史记录? - 主页面的显示内容 - 空白页 - 『时下流行』页-自定义 - 频道页 - 选择一个频道 - 尚未订阅频道 - 选择一个时下流行页 - 时下流行 - 前 50 - 最新与热门 - 显示“长按加入播放队列”提示 - 在视频详情页中,长按后台播放或悬浮窗播放按钮时显示提示 - 无法播放此串流 - 发生无法处理的播放器错误 - 播放器错误 自动恢复 - 移除 - 详情 - 音频设置 - 长按加入播放队列 - [未知] - 开始后台播放 - 开始在悬浮窗中播放 - 捐赠 - NewPipe 由志愿者开发,他们利用自己的空闲时间,为您带来最佳的用户体验。是时候回馈他们,让他们享受一杯咖啡,帮助开发者们让 NewPipe 变得更好。 - 回馈 - 官网 - 请访问 NewPipe 网站以了解更多信息和新闻。 - 视频默认国家/地区 - 切换到后台播放 - 切换到悬浮窗播放 - 切换到主页面 - 打开抽屉 - 关闭抽屉 - 第三方播放器不支持此类型链接 - 未找到视频串流 - 找不到音频串流 - 视频播放器 - 后台播放器 - 悬浮窗播放器 - 正在获取信息… - 正在加载请求的内容 - 导入数据库 - 导出数据库 - 覆盖您的当前播放历史、订阅、播放列表和设置(可选) - 导出历史记录、订阅、播放列表和设置 - 导出成功 - 导入成功 - 没有有效的 ZIP 文件 - 警告:无法导入所有文件。 - 此操作会覆盖当前设置。 - 显示信息 - 收藏 - 最近观看 - 最多观看 - 每次询问 - 新建播放列表 - 重命名 - 名称 - 添加至播放列表 - 设为播放列表封面 - 收藏播放列表 - 删除收藏 - 是否删除此播放列表? - 新建播放列表成功 - 加入播放列表成功 - 播放列表封面已更改。 - 无字幕 - 适应屏幕 - 填充屏幕 - 缩放画面 - 调试 - 自动生成 - 内存泄漏监测可能会导致应用在堆转储时无响应 - 报告超出生命周期的错误 - 强制报告处理后的未送达的 Activity 或 Fragment 生命周期之外的 Rx 异常 - 使用快速寻址(不精确) - 快速寻址定位允许播放器以较低精确度为代价换取更快的寻址定位速度。此功能不适用于以 5、15 或 25 秒为隔的寻址定位 - 自动将“接下来”视频加入播放队列 - 播放完(非循环)队列中的最后一个视频后,自动将一个相关视频添加到当前播放队列 - 没有该文件夹 - 无相似文件/内容源 - 文件不存在,或缺少读写文件权限 - 文件名不能为空 - 发生错误:%1$s - 导入 - 导入自 - 导出到 - 正在导入… - 正在导出… - 导入文件 - 先前的导出 - 无法导入订阅 - 无法导出订阅 - 从 Google takeout 导入 YouTube 订阅: -\n -\n1. 打开这个网址:%1$s -\n2. 登录谷歌账号 -\n3. 单击“包含所有数据”,然后单击“取消全选”,然后仅选择“订阅”并单击“确定” -\n4. 点击“下一步”,然后点击“创建导出作业” -\n5. 出现“下载”按钮后点击它 -\n6. 单击下面的导入文件并选择下载的 .zip 文件 -\n7.(如果 .zip 导入失败)解压 .csv文件(通常在“YouTube和YouTube Music/subscriptions/subscriptions.csv”下),点击下方的导入文件,选择解压出来的 csv 文件 - 通过输入网址或你的 ID 导入 SoundCloud 配置文件: -\n -\n1. 在浏览器中启用“电脑模式“(该网站未适配移动设备); -\n2. 打开该网站:%1$s; -\n3. 登录(如果需要); -\n4. 复制得到的配置文件下载地址。 - 你的 ID:soundcloud.com/[你的ID] - 该操作消耗大量流量, -\n -\n你想继续吗? - 关闭可禁止加载封面,节省流量和内存使用。切换该选项将立即清除内存与存储中的图片缓存 - 清空图像缓存成功 - 清空已缓存的元数据 - 清空已缓存的网页数据 - 清空元数据缓存成功 - 播放速度控制 - 节奏 - 音调 - 解除音视挂钩(可能导致失真) - 首选“打开”操作 - 打开内容时的默认操作 — %s - 没有可下载的串流 - 字幕 - 修改播放器字幕文本比例和背景样式。重启应用后生效 - 删除串流播放历史和播放痕迹记录 - 删除全部播放历史? - 清空搜索历史 - 清空搜索历史关键词 - 是否删除全部搜索历史? - 搜索历史已删除 - NewPipe 是 Copyleft 的自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证 GPLv3 或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。 - 是否要导入设置? - NewPipe 隐私政策 - NewPipe 项目非常重视您的隐私。因此,未经您的同意,应用程序不会收集任何数据。 -\nNewPipe 的隐私政策详细解释了发送崩溃报告时会发送和存储的数据。 - 阅读隐私政策 - 为了遵守欧盟的《通用数据保护条例 (GDPR)》,我们特此提醒您注意 NewPipe 的隐私政策,请您仔细阅读。 -\n您必须在同意以后才能向我们发送错误报告。 - 接受 - 拒绝 - 无限制 - 使用移动数据播放时降低分辨率 - 退出应用时最小化 - 从主播放器切换到其他应用时的操作 — %s - 静音时快进 - 比例调整 - 重置 - 曲目 - 用户 - 选择标签 - 手势控制音量 - 使用手势控制播放器的音量 - 手势控制亮度 - 使用手势控制播放器的亮度 - 视频默认语言 - 应用更新通知 - NewPipe 新版本的通知 - 外置存储不可用 - 无法下载到外部 SD 卡,修改下载文件夹位置? - 读取已保存标签时发生错误,因此使用默认标签 - 恢复默认 - 是否恢复默认值? - 更新 - 列表 - 自动 - 点击下载 - 处理中 - 生成唯一名称 - 覆盖 - 已存在一进行中并使用该名称的下载任务 - 显示错误 - 无法创建目标文件夹 - 无法创建文件 - 建立安全连接失败 - 找不到服务器 - 无法连接至服务器 - 服务器未发送数据 - 找不到 NOT FOUND - 后期处理失败 - 停止 - 最大重试次数 - 取消下载前的最多重试次数 - 切换到按流量计费的网络后中断下载 - 切换至移动数据时可能有用,虽然部分下载无法被暂停 - 事件 - 会议大会 - 显示评论 - 是否隐藏评论 - 无法加载评论 - 关闭 - 记录播放痕迹历史 - 再次打开播放过的视频时, 自动定位到上次播放时位置 - 在列表中显示历史播放位置 - 在列表中,使用底端进度条显示某一视频上次播放时的播放位置 - 已删除播放痕迹历史 - 文件已被移动或被删除 - 同名文件已存在 - 同名的已下载文件已经存在 - 无法覆盖文件 - 已暂停下载包含此名称的任务 - NewPipe 在处理文件时被关闭 - 设备上没有剩余储存空间 - 进度丢失,文件已被删除 - 连接超时 - 是否清空下载记录或删除所有下载的文件? - 限制下载并行任务数 - 同一时间内只允许进行一个下载任务 - 开始下载 - 暂停下载 - 总是询问下载位置 - 系统将询问您将每次下载的保存位置。 -\n如果要下载到外部 SD 卡,请启用系统文件夹选择器 (SAF) - 使用系统文件夹选择器 (SAF) - 存储访问框架(SAF)允许下载文件到外部 SD 卡 - 删除播放痕迹历史 - 删除所有播放痕迹历史 - 是否删除全部播放痕迹历史? - 『时下流行』页-默认 - 没有人在观看 - - %s 人在观看 - - 没有人在听 - - %s 位听众 - - 语言更改将在重启应用后生效 - PeerTube 服务器 - 设置自定义 PeerTube 服务器 - 查找你需要的服务器 %s - 添加服务器 - 输入服务器网址(URL) - 无法验证服务器 - 仅支持 HTTPS URL - 该服务器已存在 - 本地 - 最近添加 - 最受欢迎 - 自动生成的(找不到上传者) - 正在恢复 - 无法恢复此下载 - 选择一个服务器 - 快进 / 快退的寻址定位时间间隔 - 清空下载记录 - 删除下载文件 - 授予在其他应用上层显示的权限 - 应用语言 - 系统默认 - 完成后请点击“完成” - 完成 - 视频 - - %d 秒 - - 由于 ExoPlayer 的限制,寻址间隔置为 %d 秒 - 静音 - 取消静音 - 帮助 - - %d 分钟 - - - %d 小时 - - - %d 天 - - 频道组 - 订阅最后更新:%s - 未加载:%d - 正在加载 feed… - 正在处理 feed… - 选择订阅 - 未选中任何订阅 - - 已选中 %d - - 清空组名 - 您要删除该组吗? - 新建 - Feed - Feed 更新阈值 - 上次更新后,订阅被视为过期的时间 — %s - 始终更新 - 可用时使用专用 feed 获取 - 仅在某些服务中可用,通常速度更快,但返回的视频数量可能有限,而且信息通常不完整(如无视频时长、类型,无直播状态) - 启用快速模式 - 停用快速模式 - 您是否觉得 feed 加载太慢?如果是这样,请尝试启用快速加载(可在设置中修改,也可使用下面的按钮修改) -\n -\nNewPipe 提供两种 feed 加载策略: -\n•获取整个订阅频道,很慢但是很完整。 -\n•使用专用的服务端点,比较快但通常不完整 -\n -\n两者之间的区别在于,后者通常缺少一些信息,如视频的持续时间或类型(无法区分直播视频和普通视频),并且可能返回更少的视频条目。 -\n -\nYouTube 是一个通过其 RSS feed 提供这种快速方法的服务示例。 -\n -\n因此,选择哪种方式取决于您的偏好:加载速度还是信息准确。 - NewPipe 尚不支持该内容。 -\n -\n也许未来版本会支持它。 - ∞ 部视频 - 100+ 部视频 - 艺术家 - 专辑 - 歌曲 - 该视频有年龄限制! -\n -\n如果您想要观看,请在设置中启用“%1$s”。 - 由 %s - 由 %s 创建 - 频道的头像缩略图 - 是的,包括没看完的视频 - 已经看过且在之后被加入播放列表的视频将被删除。 -\n您确定吗?操作不能被撤消! - 移除看过的视频? - 移除看过的视频 - 来自服务的原始文本将在串流项目中可见 - 在项目上显示原始时间 - 启用 YouTube“受限模式” - 仅显示未分组订阅 - 播放列表页 - 尚无收藏 - 选择播放列表 - 请先检查您的要提交的问题是否已经存在。如果你创建了重复的反馈, 就会额外耗费我们用来修复这个问题的宝贵时间。 - 在 GitHub 上反馈 - 复制已整理的报告 - 显示结果:%s - 从不 - 仅在 Wi-Fi 下 - 自动开始播放 — %s - 播放队列 - 无法识别此 URL。是否用其他应用打开\? - 自动加入播放队列 - 当前播放队列将被替换 - 从一个播放器切换到另一个播放器后,你的播放队列可能会被替换 - 清空播放队列前再次确认 - - 正在缓冲 - 随机播放 - 单曲循环 - 最多可以选择三个操作显示在紧凑通知中! - 点击编辑下面的每一个通知操作。使用右方的复选框选择在紧凑通知中显示的动作,最多可以选择三个 - 第五操作按钮 - 第四操作按钮 - 第三操作按钮 - 第二操作按钮 - 第一操作按钮 - 将通知中视频缩略图的长宽比从 16:9 强制缩放到 1:1(可能会导致失真) - 强制缩放缩略图至 1:1 比例 - 显示内存泄漏 - 已加入播放队列 - 加入播放队列 - 清空与本地存储的 reCAPTCHA 验证码有关的 cookie - reCAPTCHA cookie 已被清空 - 清空 reCAPTCHA cookie - YouTube提供了“受限模式”,可以隐藏潜在的成人内容 - 展示有年龄限制的、可能不适合儿童观看的内容(比如 18+) - 让 Android 系统根据视频缩略图的主色彩给通知着色(注意,该特性仅在部分设备上可用) - 自动着色通知 - 锁屏背景和通知中使用缩略图 - 显示缩略图 - 视频哈希值计算通知 - 正在计算视频哈希值时显示的通知 - 正在计算哈希值 - 最近 - 关闭以隐藏包含有关流创建者、流内容或搜索请求的附加信息的元信息框 - 显示元数据信息 - 显示简介 - 章节 - 简介 - 相关视频 - 评论 - 显示视频描述和其他信息 - 打开方式 - 设备上没有应用可以打开 - 让应用崩溃 - 此内容仅对已付费的用户可用,因此 NewPipe 无法流式传输或下载该内容。 - 该视频仅供 YouTube Music Premium 会员使用,NewPipe 无法流式传输或下载该视频。 - 此内容是私有的,因此 NewPipe 无法流式传输或下载该内容。 - 这是 SoundCloud Go +曲目,至少在你所在的国家/地区是这样,因此 NewPipe 无法流式传输或下载它。 - 此内容在你所在的国家/地区不可用。 - 这个视频有年龄限制。 -\n由于 YouTube 针对此类视频的新政策,NewPipe 无法访问其任何视频流,因此无法播放该视频。 - 处理 - 电台 - 精选 - 自动(系统主题) - 下载已开始 - 在此选择您最喜欢的夜间主题 - 选择你最喜欢的夜间主题 — %s - 夜间主题 - 显示频道详情 - 如果遇到黑屏或视频播放卡顿的情况,请停用媒体隧道 - 停用媒体隧道 - 停用简介中的文本选择功能 - 内部 - 私享 - 未分类 - 公开 - 缩略图 URL - 所在服务器 - 支持 - 语言 - 年龄限制 - 私有性 - 许可 - 标签 - 类别 - 启用简介中的文本选择功能 - 你现在可以选择简介中的文本,注意,在选择模式下,页面可能会闪烁,链接可能无法点击。 - 打开网站 - %s 提供这个原因: - 账号被终止 - 快速 Feed 模式不提供关于这个的更多信息。 - 作者账号已被终止。 -\nNewPipe 今后将无法加载此 Feed。 -\n你要退订此频道吗? - 无法加载“%s”的 Feed。 - 加载 Feed 时出错 - 仅 Android 10 及以上版本支持“存储访问框架” - Android KitKat 及更低版本不支持“存储访问框架” - 你会被问到在哪里保存每个下载 - 尚未设置下载文件夹,现在选择默认下载文件夹 - 平板模式 - 显示已观看的项目 - 关闭 - 开启 - 评论功能已停用 - 进度条缩略图预览 - 不显示 - 低品质(较小) - 高品质(较大) - 被创作者喜爱 - 标记为已观看 - 在图像顶部显示毕加索彩带,指示其来源:红色代表网络,蓝色代表磁盘,绿色代表内存 - 显示图像指示器 - 远程搜索建议 - 本地搜索建议 - - 删除了 %1$s 个下载 - - - 完成了 %s 个下载 - - 滑动即可删除项目 - 若自动旋转被锁定,不在以小窗播放器形式中播放视频,而直接切换到全屏模式。仍可以通过退出全屏以切换至小窗播放器 - 以全屏启动主播放器 - 已添加为下一个播放 - 下一个播放 - 处理中…可能需要一些时间 - 手动检查新版本 - 检查更新中… - 检查更新 - 新订阅源条目 - 显示\"使播放器崩溃\" - 在使用播放器时显示一个崩溃选项 - 使播放器崩溃 - 错误报告通知 - 提示报告错误的通知 - 发生错误,详见通知 - 显示错误警示SnackBar - 创建一条错误通知 - 找不到适合此操作的文件管理器。 -\n请安装一文件管理器或尝试在下载设置中禁用“%s”。 - 找不到适合此操作的文件管理器。 -\n请安装与存储访问框架(SAF)兼容的文件管理器。 - NewPipe 遇到了一个错误,点击此处报告此错误 - 置顶评论 - LeakCanary 不可用 - 更改加载间隔的大小(当前为 %s),较低的值可以加快视频的首次加载速度。更改需要重启播放器。 - ExoPlayer 默认 - 配置当前正在播放的串流的通知 - 新串流通知 - 检查频率 - 所需的网络连接 - 通知已被禁用 - 你刚刚订阅了此频道 - - 全选 - 播放器通知 - 通知 - 新的串流 - - %s 条新串流 - - - 被订阅的新串流的通知 - 正在加载串流详情… - 检查新串流 - 任何网络 - 清除所有下载的文件? - 获取通知 - 来自订阅的新串流的通知 - 半音 - 百分比 - 未知格式 - 没有音频流可用于外部播放器 - 选择外部播放器画质 - 外部播放器不支持所选串流 - 没有视频流可用于外部播放器 - 不显示下载器尚不支持的串流 - 未知画质 - \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index dc13a16c2..5ac2b1db8 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -446,7 +446,7 @@ Лиценз %s посочва следната причина: Маркери - Достъпност + Поверителност Език Вътрешен Включен @@ -553,4 +553,17 @@ Показвай цветни Picasso-панделки в горната част на изображенията като индикатор за техния произход (червен – от мрежата, син – от диска и червен – от паметта) Автоматична (тази на устройството) Мащабиране на миниатюрата в известието от 16:9 към 1:1 формат (възможни са изкривявания) + Покажи гледани + Избете плейлист + Известия + Изчистване на бисквитките от reCAPTCHA + Бисквитките от reCAPTCHA бяха почистени + Проверяване за актуализации… + , + Провери за актуализации + Процент + Неизвестно качество + Неизвестен формат + Наскоро добавено + Буфериране \ No newline at end of file diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 02d35d384..1de4d3a50 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -459,4 +459,5 @@ দেখা হিসেবে মার্ক করো ট্যাগসমূহ অ্যান্ড্রয়েডকে থাম্বনেইলের প্রধান রং অনুযায়ী রঙিন করুন (উল্লেখ্য যে, এটি সব ডিভাইসে উপলব্ধ নয়) + অজানা ধরন \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index a1acf9fd1..6a634a3a4 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -622,4 +622,6 @@ বাহ্যিক প্লেয়ারের জন্য কোনো অডিও স্ট্রিম নেই বাহ্যিক প্লেয়ারের জন্য কোনো ভিডিও স্ট্রিম নেই অজ্ঞাত মান + নতুন ধারার বিজ্ঞপ্তি + নেটওয়ার্ক সংযোগ দরকার \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index a42e68940..099203790 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -675,7 +675,6 @@ S\'ha suprimit %1$s baixada S\'han suprimit %1$s baixades - El \"Sistema d\'Accés a l\'Emmagatzematge\" no està implementat a Android KitKat i a versions anteriors A partir de l\'Android 10 només s\'admet el \"Sistema d\'Accés a l\'Emmagatzematge\" Elements de feed nous El mode d\'alimentació ràpida no proporciona més informació sobre això. diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 9796404b4..4c53c11b9 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -557,8 +557,8 @@ سێیه‌م كرداری دوگمه‌ دووه‌م كرداری دوگمه‌ یه‌كه‌م كرداری دوگمه‌ - وێنۆچكه‌ی ڤیدیۆ پێوانه‌ ده‌كرێته‌وه‌ له‌ پەیامەکاندا له‌ ڕه‌هه‌ندی 16:9 ه‌وه‌ بۆ ڕه‌هه‌ندی 1:1 - پێوانەكردنی وێنۆچكه‌ بۆ ڕه‌هه‌ندی 1:1 + وێنۆچكه‌ی ڤیدیۆ دەبڕدرێت له‌ پەیامەکاندا له‌ ڕه‌هه‌ندی 16:9 ه‌وه‌ بۆ ڕه‌هه‌ندی 1:1 + بڕینی وێنۆچكه‌ بۆ ڕه‌هه‌ندی ڕێژەیی 1:1 پیشاندانی ئەنجامەکانی: %s كردنه‌وه‌ له‌ ناكارایبكه‌ بۆ شاردنه‌وه‌ی چوارگۆشه‌ی مێتا و زانیاری زیاده‌ له‌سه‌ر بابەتی په‌خش و داواكاری گه‌ڕان @@ -625,7 +625,6 @@ بۆ دابه‌زاندنی هه‌ر بابه‌تێك پرست پێ ده‌كرێت له‌باره‌ی شوێنی دابه‌زاندنیان ناكاراكردنی تونێلكردنی میدیا ئه‌و بابه‌تانه‌ی نه‌گونجاون بۆ منداڵان پیشان بدرێن كه‌ سنووری ته‌مه‌ن ده‌یانگرێته‌وه‌ (وه‌ك +18) - \'Storage Access Framework\' پشتگیری نه‌كراوه‌ له‌سه‌ر وه‌شانه‌كانی ئه‌ندرۆید كیتكات و نزمتر كڕاشی به‌رنامه‌كه‌ پیشاندانی دزه‌كردنی بیرگه‌ له‌نۆبه‌ت دانرا diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ea6d292d8..41bfafaac 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -366,7 +366,7 @@ Vyčkávání Pozastaveno ve frontě - post-processing + zpracování Zařadit do fronty Akce odmítnuta systémem Stahování se nezdařilo @@ -384,7 +384,7 @@ Server neposílá data Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1 Nenalezeno - Post-processing selhal + Zpracování selhalo Zastavit Maximální počet pokusů o opakování Maximální počet pokusů před zrušením stahování @@ -570,8 +570,8 @@ Třetí akční tlačítko Druhé akční tlačítko První akční tlačítko - Zmenšit miniaturu videa zobrazenou v oznámení z poměru stran 16: 9 na 1: 1 (může způsobit zkreslení) - Změnit poměr stran miniatury na 1:1 + Oříznout miniaturu videa zobrazenou v oznámení z poměru stran 16:9 na 1:1 + Oříznout poměr stran miniatury na 1:1 Ukázat memory leaks Zařazeno do fronty Zařadit do fronty @@ -584,10 +584,10 @@ Přibarvit oznámení Použít miniaturu pro pozadí zamknuté obrazovky a oznámení Zobrazit miniaturu - Oznámení o hašování videa + Oznámení o hashování Nedávné - Počítám haš - Oznámení o postupu hašování videa + Počítám hash + Oznámení o postupu hashování videa Vypnout, abyste skryli rámečky s meta informací s údaji o autorovi streamu, obsahu streamu nebo požadavků hledání Zobrazit meta informaci Kapitoly @@ -596,7 +596,7 @@ Zobrazit popis Otevřít s Na Vašem zařízení není aplikace, která to umí otevřít - Podobné strýmy + Podobné položky Vypnout pro skrytí popisu videa a doplňkové informace Zbořit aplikaci Stahování bylo zahájeno @@ -643,7 +643,6 @@ Feed pro \'%s\' nemohl být načten. Chyba při načítání feedu Počínaje Android 10 je podporován pouze \"Storage Access Framework\" - \"Storage Access Framework\" není podporován na KitKat a níže Budete dotázáni, kde uložit každý stažený soubor Adresář pro stažené soubory dosud nenastaven, zvolte, prosím, výchozí adresář nyní Vypnuto @@ -684,43 +683,52 @@ Vytvořit oznámení o chybě Kontrola aktualizací… Ukázat „Shodit přehrávač“ - Nové položky feedů + Nové položky Pro tuto akci nebyl nalezen žádný vhodný správce souborů. -\nProsím, nainstalujte správce souborů kompatibilní se Storage Access Framework. +\nNainstalujte správce souborů kompatibilní se Storage Access Framework Oznámení o hlášení chyb Oznámení za účelem hlášení chyb Pro tuto akci nebyl nalezen žádný vhodný správce souborů. -\nProsím, nainstalujte správce souborů nebo zkuste vypnout %s v nastaveních stahování. +\nNainstalujte správce souborů nebo zkuste vypnout \'%s\' v nastavení stahování Ukáže volbu pro zřícení během používání přehrávače Ukázat krátké oznámení o chybě Připnutý komentář Shodit přehrávač - Změnit interval načítání (aktuálně %s). Menší hodnota může zrychlit počáteční načítání videa. Změna vyžaduje restart přehrávače. + Změnit interval načítání (aktuálně %s). Menší hodnota může zrychlit počáteční načítání videa. Změna vyžaduje restart přehrávače LeakCanary není dostupné Výchozí ExoPlayer - Nastavit oznámení k právě přehrávanému strýmu - Oznámení o nových strýmech - Oznámit o nových strýmech k objednání + Nastavit oznámení o právě přehrávaném streamu + Oznámení o nových streamech + Oznámit o nových strýmech od vašich odběrů Frekvence kontroly Jakákoli síť Nutné síťové připojení Smazat všechny stažené soubory z disku\? Objednali jste si nyní tento kanál Všechny přepnout - Nové strýmy - Oznámení o nových strýmech k objednání - Spustit kontrolu nových strýmů - Oznámení přehrávače + Nové streamy + Oznámení o nových streamech od odběrů + Spustit kontrolu nových streamů + Upozornění přehrávače Oznámení - Načítám podrobnosti o strýmu… + Načítám podrobnosti o streamu… Oznámení jsou vypnuta Přijímat oznámení , - %s nový strým - %s nové strýmy - %s nových strýmů + %s nový stream + %s nové streamy + %s nových streamů Procento Půltón + Velikost intervalu načtení přehrávání + Vybraný stream není podporován externími přehrávači + U externích přehrávačů nejsou dostupné žádné zvukové streamy + Neznámý formát + Neznámá kvalita + Zobrazit nadcházející videa + Streamy, které zatím nejsou podporovány systémem stahování, nebudou zobrazeny + Vyberte kvalitu pro externí přehrávače + U externích přehrávačů nejsou dostupné žádné video streamy \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b91fec84a..e57c1874e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Veröffentlicht am %1$s - Kein Stream-Player gefunden. Möchtest du den VLC installieren\? + Kein Stream-Player gefunden. Möchtest du VLC installieren\? Installieren Abbrechen Im Browser öffnen @@ -161,7 +161,7 @@ Die meisten Sonderzeichen Wiedergabe fortsetzen Player - Nichts hier außer dem Zirpen der Grillen + Nichts hier, außer dem Zirpen der Grillen Möchtest du dieses Element aus dem Suchverlauf löschen\? Leere Seite Einen Kanal auswählen @@ -558,8 +558,8 @@ Dritte Aktionstaste Zweite Aktionstaste Erste Aktionstaste - Skaliert das in der Benachrichtigung angezeigte Vorschaubild von 16:9 auf ein 1:1 Seitenverhältnis (kann zu Verzerrungen führen) - Vorschaubild auf 1:1 Seitenverhältnis skalieren + Beschneidet das in der Benachrichtigung angezeigte Video-Vorschaubild von 16:9 auf ein 1:1 Seitenverhältnis + Vorschaubild auf 1:1 Seitenverhältnis zuschneiden Zufällig Puffern Wiederholen @@ -638,7 +638,6 @@ Noch kein Downloadordner festgelegt, wähle jetzt den Standard-Downloadordner Webseite öffnen Ab Android 10 wird nur noch „Storage Access Framework“ unterstützt - Das „Storage Access Framework“ wird auf Android KitKat und niedriger nicht unterstützt Du wirst jedes Mal gefragt werden, wohin der Download gespeichert werden soll Fehler beim Laden des Feeds Konnte Feed für \'%s\' nicht laden. @@ -681,12 +680,12 @@ Eine Fehlermeldung erstellen Fehler-Kurzmeldung anzeigen Es wurde kein geeigneter Dateimanager für diese Aktion gefunden. -\nBitte installiere einen Dateimanager oder versuche, \'%s\' in den Downloadeinstellungen zu deaktivieren. +\nBitte installiere einen Dateimanager oder versuche, \'%s\' in den Downloadeinstellungen zu deaktivieren Es wurde kein geeigneter Dateimanager für diese Aktion gefunden. -\nBitte installiere einen Storage Access Framework kompatiblen Dateimanager. +\nBitte installiere einen Storage Access Framework kompatiblen Dateimanager Angehefteter Kommentar LeakCanary ist nicht verfügbar - Ändern der Größe des Ladeintervalls (derzeit %s). Ein niedrigerer Wert kann das anfängliche Laden des Videos beschleunigen. Änderungen erfordern einen Neustart des Players. + Ändern der Größe des Ladeintervalls (derzeit %s). Ein niedrigerer Wert kann das anfängliche Laden des Videos beschleunigen. Änderungen erfordern einen Neustart des Players ExoPlayer Standard Benachrichtigungen Benachrichtigen über neue abonnierbare Streams @@ -718,4 +717,5 @@ Streams, die noch nicht vom Downloader unterstützt werden, werden nicht angezeigt Der ausgewählte Stream wird von externen Playern nicht unterstützt Größe des Ladeintervalls für die Wiedergabe + Zukünftige Videos anzeigen \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 06c7e4ae4..b1b11e24d 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -498,8 +498,8 @@ Κουμπί τρίτης ενέργειας Κουμπί δεύτερης ενέργειας Κουμπί πρώτης ενέργειας - Κλιμάκωση της μικρογραφίας βίντεο που εμφανίζεται στην ειδοποίηση από 16:9 σε αναλογία διαστάσεων 1:1 (μπορεί να προκαλέσει παραμορφώσεις) - Κλιμάκωση μικρογραφίας σε αναλογία διαστάσεων 1:1 + Περικοπή της μικρογραφίας βίντεο που εμφανίζεται στην ειδοποίηση από 16:9 σε αναλογία διαστάσεων 1:1 + Περικοπή μικρογραφίας σε αναλογία διαστάσεων 1:1 Φόρτωση Πιστεύετε ότι η ροή φορτώνει πολύ αργά; Δοκιμάστε να ενεργοποιήσετε τη γρήγορη φόρτωση (από τις ρυθμίσεις ή πατώντας το παρακάτω κουμπί). \n @@ -632,7 +632,6 @@ Αδυναμία φόρτωσης τροφοδοσίας για \'%s\'. Σφάλμα φόρτωσης τροφοδοσίας Από το Android 10 και μετά, μόνο το SAF υποστηρίζεται - Το «Πλαίσιο Πρόσβασης Αποθήκευσης» δεν υποστηρίζεται σε Android KitKat και παλαιότερο Θα ερωτηθείτε πού να αποθηκεύσετε κάθε λήψη Δεν έχει ορισθεί φάκελος λήψεων ακόμα, eπιλέξτε τον προεπιλεγμένο φάκελο τώρα Host @@ -717,4 +716,6 @@ Επιλογή ποιότητας εξωτερικών αναπαραγωγών Άγνωστος τύπος αρχείου Άγνωστη ποιότητα + Μέγεθος διαστήματος φόρτωσης αναπαραγωγής + Εμφάνιση μελλοντικών βίντεο \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 51d61c92d..7b7dfe99e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -544,7 +544,7 @@ Copiar informe con formato Mostrando resultados para: %s Orden aleatorio - Escalar miniatura a relación de aspecto 1:1 + Recortar miniatura a relación de aspecto 1:1 Nunca Solo en Wi-Fi Comenzar reproducción automáticamente — %s @@ -558,13 +558,13 @@ Almacenar en memoria (búfer) Repetir ¡Puedes seleccionar como máximo tres acciones para mostrar en la notificación compacta! - Edita cada acción de notificación debajo pulsando sobre ella. Selecciona hasta tres de ellas para que aparezcan en la notificación compacta usando las casillas de verificación a la derecha + Edita cada una de las acciones en la notificación pulsando sobre ellas. Selecciona hasta tres de ellas para mostrarlas en la notificación compacta usando las casillas de verificación de la derecha. Botón de quinta acción Botón de cuarta acción Botón de tercera acción Botón de segunda acción Botón de primera acción - Escalar la relación de aspecto de la miniatura del vídeo mostrada en la notificación de 16:9 a 1:1 (puede ocasionar distorsiones) + Recortar la relación de aspecto de la miniatura del vídeo mostrada en la notificación de 16:9 a 1:1 Vaciar las cookies que NewPipe guarda al resolver un reCAPTCHA Mostrar contenido inapropiado para niños porque tiene un limite de edad (como 18+) Mostrar pérdidas de memoria @@ -574,7 +574,7 @@ Limpiar las cookies reCAPTCHA YouTube provee un «Modo restringido», el cual oculta contenido potencialmente solo apto para adultos Ajustar color de notificación - Permitir a Android personalizar el color de la notificación con el color principal de la imagen (ten en cuenta que esta opción no funciona en todos los dispositivos) + Permitir a Android personalizar el color de la notificación usando el color principal de la miniatura (ten en cuenta que esta opción no funciona en todos los dispositivos) Usar miniatura como fondo de pantalla de bloqueo y notificaciones Mostrar vista previa Desactivar para ocultar información adicional sobre el creador o contenido de la transmisión @@ -603,7 +603,7 @@ La descarga ha comenzado Resolver Puedes seleccionar tu tema nocturno favorito a continuación - Tema de Noche + Modo oscuro Selecciona tu tema nocturno favorito — %s Automático (tema del dispositivo) Mostrar detalles del canal @@ -633,7 +633,6 @@ \n¿Desea desuscribirse de este canal\? Error al cargar el muro Desde Android 10 solo el \'Sistema de Acceso al Almacenamiento\' es soportado - El \'Sistema de Acceso al Almacenamiento\' no es sorportado en Android KitKat o versiones anteriores Se le preguntará dónde guardar cada descarga Deshabilitar el túnel de medios si experimenta una pantalla negra o interrupciones en la reproduccción de videos Deshabilitar el túnel de medios @@ -681,14 +680,12 @@ Se produjo un error, vea la notificación Crear una notificación de error Mostrar un snackbar de error - No se encontró ningún gestor de archivos adecuado para esta acción. -\nPor favor instale un gestor de archivos o intente deshabilitar \"%s\" en lo ajustes de descargas. - No se encontró ningún gestor de archivos adecuado para esta acción. -\nPor favor instale un gestor de archivos compatible con \"Sistema de Acceso al Almacenamiento\". + No se encontró ningún administrador de archivos apropiado para esta acción. Instale un administrador de archivos o intente deshabilitar \'%s\' en la configuración de descarga + No se encontró ningún administrador de archivos apropiado para esta acción. Instale un administrador de archivos compatible con Storage Access Framework Comentario fijado LeakCanary no está disponible ExoPlayer valor por defecto - Cambia el tamaño del intervalo de carga (actualmente %s). Un valor más bajo puede acelerar la carga inicial del vídeo. Los cambios requieren un reinicio del reproductor. + Cambie el tamaño del intervalo de carga (actualmente %s). Un valor más bajo puede acelerar la carga inicial de video. Los cambios requieren un reinicio del reproductor Notificaciones Nuevos streams Notificación del reproductor @@ -720,4 +717,6 @@ Elija la calidad para reproductores externos Formato desconocido Calidad desconocida + Mostrar videos futuros + Tamaño del intervalo de carga de reproducción \ No newline at end of file diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 7cb96405a..2d776155c 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -426,8 +426,8 @@ Kolmas tegevusnupp Teine tegevusnupp Esimene tegevusnupp - Skaleeri teavituses kuvatav video pisipilt 16:9 külgede suhtest 1:1 suhtesse (võib põhjustada häireid) - Skaleeri pisipilt 1:1 küljesuhtesse + Kadreeri teavituses kuvatav video pisipilt 16:9 külgede suhtest 1:1 suhtesse + Kadreeri pisipilt 1:1 küljesuhtesse Arvutan räsi Hiljutised Kirjeldus @@ -632,7 +632,6 @@ Voog Vali eksemplar Android 10st alates on toetatud ainult salvestusjuurdepääsu raamistik \'Storage Access Framework\' - Android KitKat ja vanemad versioonid ei toeta salvestusjuurdepääsu raamistikku \'Storage Access Framework\' Sinult küsitakse iga kord, kuhu alla laadimine salvestada Südamlik autor Kas sinu meelest on voo laadimine aeglane\? Sel juhul proovi lubada kiire laadimine (seda saad muuta seadetes või vajutades allolevat nuppu). @@ -717,4 +716,6 @@ Valitud meediavood ei ole toetatud välises pleieris Protsent Pooltoon + Taasesituseks vajalike andmete laadimise samm + Näita tulevasi videoid \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 80e2d5a04..c2d92c517 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -562,8 +562,8 @@ Hirugarren ekintzaren botoia Bigarren ekintzaren botoia Lehenego ekintzaren botoia - Eskalatu jakinarazpenetan erakusten den bideo miniaturaren formatu-ratioa 16:9tik 1:1era (distortsioak sor ditzake) - Miniatura 1:1 formatu-ratiora eskalatu + Ebaki jakinarazpenetan erakusten den bideo miniaturaren formatu-ratioa 16:9tik 1:1era + Miniatura 1:1 formatu-ratiora ebaki %s bilaketaren erantzunak erakusten Ilaran jarri da Jarri ilaran @@ -626,7 +626,6 @@ Ezin izan da \'%s\' jarioa kargatu. Errorea jarioa kargatzean Android 10etik aurrera \'Biltegiaren Sarrera Framework\'a soilik onartzen da - \'Biltegiaren Sarrera Framework\'a ez da Android KitKat eta aurreko bertsioetan onartzen Non gorde galdetuko zaizu deskarga bakoitzean Ez da deskargatzeko karpetarik ezarri oraindik, aukeratu lehenetsitako deskargatzeko karpeta orain Pribatutasuna @@ -710,4 +709,13 @@ Beharrezko sare konexioa Portzentaia Semitonoa + Erreprodukzioaren kargatze-tartearen tamaina + Deskargatzaileak onartzen ez dituen jarioak ez dira erakusten + Hautatutako jarioa ez dago kanpoko erreproduzigailu batengatik onartuta + Ez dago kanpoko erreproduzigailu batengatik onartuta dagoen audio jariorik + Ez dago kanpoko erreproduzigailu batengatik onartuta dagoen bideo jariorik + Formatu ezezaguna + Kalitate ezezaguna + Erakutsi etorkizuneko bideoak + Hautatu kanpoko erreproduzigailuen kalitatea \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a00c64ad0..6b607b6ae 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -557,8 +557,8 @@ سومین دکمه کنشی دومین دکمه کنشی اولین دکمه کنشی - تصویر بندانگشتی ویدیو که در اعلان نمایش می‌یابد، از نسبت ۱۶:۹ به ۱:۱ تغییر اندازه پیدا کند (ممکن است منجر به اعوجاج شود) - تغییر مقیاس تصویر بندانگشتی به نسبت ۱:۱ + تصویر بندانگشتی ویدیو که در اعلان نمایش می‌یابد، از نسبت ۱۶:۹ به ۱:۱ بریده می‌شود + برش تصویر بندانگشتی به نسبت ۱:۱ کیفیت پایین (کوچک‌تر) کیفیت بالا (بزرگ‌تر) نظرها از کار افتاده‌اند @@ -636,7 +636,6 @@ اکنون می‌توانید متن درون شرخ را برگزینید. به یاد داشته باشید که در حالت گزینش، ممکن است صفحه چشمک زده و پیوندها قابل کلیک نباشند. هنوز شاخهٔ بارگیری‌ای تنظیم نشده. اکنون شاخهٔ بارگیری پیش‌گزیده را برگزینید برای ذخیرهٔ هر بارگیری از شما پرسیده خواهد شد - «چارچوب دسترسی ذخیره» روی اندروید کیت‌کت و پایین‌تر پشتیبانی نمی‌شود از اندروید ۱۰، تنها «چارچوب دسترسی ذخیره» پشتیبانی می‌شود نتوانست خوراک را برای «%s» بار کند. حساب این نگارنده نابود شده است. @@ -717,4 +716,6 @@ گزینش کیفیت برای پخش‌کننده‌های خارجی قالب ناشناخته کیفیت ناشناخته + اندازهٔ دورهٔ بار کردن پخش + نمایش ویدیوهای آینده \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 8eef663b0..af19e4f12 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -638,7 +638,6 @@ \nHaluatko poistaa kanavan tilauksesta\? Ei voitu ladata syötettä hakusanalle \'%s\'. Virhe syötteen lataamisessa - \'Storage Access Framework\' ei ole tuettu Android KitKatissa tai vanhemmissa versioissa Sinulta kysytään joka kerta, minne tiedosto ladataan Älä näytä Matala laatu (pienempi) diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index d8c6682d6..758e522cd 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -12,7 +12,7 @@ I-download I-download ang stream file Maghanap - Ayos ng App + Pagsasaayos \"%1$s\" ba ang tinutukoy mo\? Ibahagi sa Gumamit ng ibang video player @@ -30,7 +30,7 @@ Pumili ng Tab Anong Bago Likuran - Popup + Naka-lutang Idagdag sa Download folder ng mga video Pumili ng download folder para sa mga video file @@ -257,4 +257,7 @@ Notipikasyon ng player Mga track Mga gumagamit + Hangganan ng Edad + Oo, pati na rin ang mga napanood nang video + Kusa (tema ng device) \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 327877651..63a2e19d1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -6,7 +6,7 @@ Télécharger Dossier de téléchargement vidéo Choisissez le dossier de téléchargement des vidéos - Les fichiers vidéo téléchargés sont stockées ici + Les vidéos téléchargées sont stockées ici Installer Installer l’application Kore manquante \? Aucun lecteur de flux trouvé. Installer VLC \? @@ -99,7 +99,7 @@ Seuls certains appareils peuvent lire des vidéos 2K/4K Format vidéo par défaut Mémoriser les propriétés de la fenêtre flottante - Mémorise les dernières taille et position de la fenêtre flottante + Mémoriser les dernières taille et position de la fenêtre flottante Effacer G Le son peut être absent à certaines définitions @@ -551,7 +551,7 @@ Impossible de reconnaitre l’URL fournie. Voulez-vous l’ouvrir avec une autre application \? Ajouter automatiquement à la liste de lecture La liste de lecture du lecteur actif sera remplacée - Confirmer avant de supprimer la liste de lecture + Confirmer avant de supprimer une liste de lecture Rien Chargement Lire aléatoirement @@ -563,8 +563,8 @@ Troisième bouton d’action Deuxième bouton d’action Premier bouton d’action - Mettre à l\'échelle la miniature de la vidéo affichée dans la notification du format 16:9 au format 1:1 (peut provoquer des déformations) - Redimensionner la miniature au format 1:1 + Recadrer la miniature de la vidéo affichée dans la notification du format 16:9 au format 1:1 + Recadrer la miniature au format 1:1 Afficher les fuites de mémoire Ajouté à la file d’attente Ajouter à la file d’attente @@ -640,7 +640,6 @@ \nNewPipe ne sera plus en mesure de charger ce flux à l’avenir. \nSouhaitez-vous vous désabonner de cette chaîne \? À partir d’Android 10, seule « l’Infrastructure d’accès au stockage  » est prise en charge - L’« Infrastructure d’accès au stockage » n’est pas prise en charge par Android KitKat et les versions antérieures Le mode flux rapide ne fournit pas plus d’info à ce sujet. Les commentaires sont désactivés Ne pas afficher @@ -664,8 +663,8 @@ Balayez un élément pour le supprimer Ne pas lancer les vidéos dans le mini lecteur mais directement en plein écran si la rotation automatique est verrouillée. Vous pouvez toujours accéder au mini-lecteur en quittant le mode plein écran Lancer le lecteur principal en plein écran - Ajouter à la liste de lecture - Suivant dans la liste de lecture + Lire consécutivement + Flux placé à la suite Traitement en cours… Veuillez patienter Vérifier manuellement de nouvelles versions Vérification des mises à jour… @@ -720,4 +719,5 @@ Le flux séléctionné n\'est pas supporté par les lecteurs externes Aucun flux vidéo n\'est disponible pour les lecteurs externes Taille de l\'intervalle de chargement de la lecture + Afficher les futures vidéos \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 06b35acd3..bebbf2483 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -548,8 +548,8 @@ Terceiro botón de acción Cuarto botón de acción Quinto botón de acción - Escalar a miniatura do vídeo amosado na notificación da relación de aspecto 16:9 a 1:1 (pode intruducir distorsións) - Escala miniatura á relación de aspecto 1:1 + Cortar a miniatura do vídeo amosado na notificación da relación de aspecto 16:9 a 1:1 + Cortar miniatura á relación de aspecto 1:1 Apagado Modo tableta Abrir sitio Web @@ -657,8 +657,7 @@ Inciar reprodutor principal en pantalla completa Non iniciar vídeos no reprodutor mini, mais cambiar a pantalla completa directamente, se a rotación estiver bloqueada. Aínda pode acceder o reprodutor mini ao saír da pantalla completa Deslice os elementos para removelos - Non foi atopado ningún xestor de arquivos adecuado para esta acción. -\nPor favor instale un ou tente deshabilitar \'%s\' nas opcións de descarregamento. + Non se atopou ningún xestor de ficheiros axeitado para esta acción. Instala un xestor de ficheiros ou intenta desactivar \'%s\' na configuración de descarga Non foi atopado ningún xestor de arquivos adecuado para esta acción. \nPor favor instale un compatíbel co Sistema de Acceso ao Almacenamento. Valorado polo creador @@ -678,7 +677,6 @@ Procesando... Pode devagar un momento Crear unha notificación de erro Amosar fitas coloridas de Picasso na cima das imaxes que indican a súa fonte: vermello para a rede, azul para o disco e verde para a memoria - O \'Sistema de Acceso ao almacenamento\' non está soportado en Android KitKat e anteriores Novos elementos Predefinido do ExoPlayer Amosar \"Travar o reprodutor\" @@ -710,4 +708,13 @@ As notificacións están desactivadas Recibir notificacións Agora está subscrito a esta canle + Emisións non soportadas polo descarregador non son mostradas + Seleccione a calidade para reprodutores externos + Formato descoñecido + Calidade descoñecida + Tamaño do intervalo de carregamento da reprodución + As emisións seleccionadas non son soportadas polos reprodutores externos + Non hai emisións de vídeo dispoñíbeis para reprodutores externos + Ver futuros vídeos + Non hai emisións de audio dispoñíbeis para reprodutores externos \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index b0ece06f1..a80d66849 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -580,8 +580,8 @@ כפתור פעולה שלישי כפתור פעולה ראשון כפתור פעולה שני - לשנות את יחס התצוגה הממוזערת שמופיעה בהתראות מיחס תצוגה של 16:9 ל־1:1 (עשוי לעוות את התמונה) - שינוי גודל התצוגה הממוזערת ליחס תצוגה 1:1 + חיתוך התצוגה הממוזערת שמופיעה בהתראות מיחס תצוגה של 16:9 ל־1:1 + חיתוך התצוגה הממוזערת ליחס תצוגה 1:1 הצגת דליפות זיכרון נוסף לתור הוספה לתור @@ -653,7 +653,6 @@ לא ניתן לטעון את ההזנה עבור ‚%s’. שגיאה בהורדת ההזנה התמיכה ב‚תשתית גישה לאחסון’ נתמכת מ־Android 10 בלבד - ‚תשתית הגישה לאחסון’ אינה נתמכת על ידי Android KitKat ומטה תופיע שאלה לאן לשמור כל הורדה טרם הוגדרה תיקיית הורדה, נא לבחור את תיקיית ההורדה כעת כבוי @@ -702,14 +701,14 @@ NewPipe נתקל בשגיאה, לחיצה תדווח על כך אירעה שגיאה, נא לקרוא את ההתראה לא נמצא מנהל קבצים מתאים לפעולה זו. -\nנא להתקין מנהל קבצים או לנסות להשבית את ‚%s’ בהגדרות ההורדה. +\nנא להתקין מנהל קבצים או לנסות להשבית את ‚%s’ בהגדרות ההורדה התראת דיווח שגיאה לא נמצאו מנהלי קבצים שמתאימים לפעולה הזאת. -\nנא להתקין מנהל קבצים שתומך בתשתית גישה לאחסון. +\nנא להתקין מנהל קבצים שתומך בתשתית גישה לאחסון הערה ננעצה LeakCanary אינה זמינה ברירת מחדל של ExoPlayer - שינוי גודל מרווח הטעינה (כרגע %s). ערך נמוך יותר עשוי להאיץ את טעינת הווידאו הראשונית. שינויים דורשים את הפעלת הנגן מחדש. + שינוי גודל מרווח הטעינה (כרגע %s). ערך נמוך יותר עשוי להאיץ את טעינת הווידאו הראשונית. שינויים דורשים את הפעלת הנגן מחדש התראות על תזרימים חדשים להרשמה תדירות בדיקה נדרש חיבור לרשת @@ -743,4 +742,6 @@ אין תזרימי וידאו שזמינים לנגנים חיצוניים בחירת איכות לנגנים חיצוניים תצורה לא מוכרת + גודל משך טעינת נגינה + הצגת סרטונים עתידיים \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 1d76be613..cd0cf6f52 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -10,7 +10,7 @@ सर्च करे सेटिंग्स सब्सक्राइब करें - सब्सक्राइबड + सस्क्राइब्ड चैनल अनसब्सक्राइब हुआ सब्सक्रिप्शनस बैकग्राउंड @@ -57,10 +57,10 @@ पॉपअप का डिफ़ॉल्ट रिज़ॉल्यूशन उच्च रिज़ॉल्यूशनस दिखाएं केवल कुछ ही डिवाइस 2K/4K मे विडियो चला सकते हैं - Kodi से चलायें + Kodi मे प्ले करे Kore ऐप नहीं मिली, इसे इनस्टॉल करें\? - \"Kodi से चलायें\" वाला विकल्प दिखाएँ - Kodi मीडिया सेंटर से विडियो चलाने के लिए विकल्प प्रदर्शित करें + \"Kodi मे प्ले करे\" वाला विकल्प दिखाएँ + Kodi मीडिया सेंटर से विडियो प्ले करने के लिए विकल्प प्रदर्शित करें डिफ़ॉल्ट ऑडियो फॉर्मेट डिफ़ॉल्ट विडियो फॉर्मेट ऐप थीम @@ -187,7 +187,7 @@ स्ट्रीम फाइल डाउनलोड करें जानकारी दिखाएं बुकमार्क किये गए प्लेलिस्टस - में जोड़े + में एड करे डिफ़ॉल्ट देश का विषय हमेशा के लिए सिर्फ एक बार के लिए @@ -518,9 +518,9 @@ विडीओ हैशिंग की प्रगति की सूचना वीडियो हैश अधिसूचना स्ट्रीम निर्माता, स्ट्रीम विषय सूची या खोज अनुरोध के बारे में अतिरिक्त जानकारी के साथ मेटा जानकारी बक्से को छिपाने के लिए बंद करें. - मेटा जानकारी दिखाएँ + Meta info दिखाएँ वीडियो का विवरण और अतिरिक्त जानकारी छिपाने के लिए इसे बंद करें - विवरण दिखाएं + डिस्क्रिप्शन दिखाएं सक्रिय प्लेअर की क़तार बदल दी जाएगी एक प्लेअर से दूसरे प्लेअर में जाने से आपकी कतार बदल सकती है मे खोलें diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 0ca0c75dc..439835e06 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,63 +1,63 @@ - Za početak dodirnite povećalo. + Počni dodirom na povećalo. Objavljeno %1$s - Reproduktor za stream nije pronađen. Instalirati VLC\? + Nije pronađen nijedan player streamova. Želiš li instalirati VLC\? Instaliraj Odustani Otvori u pregledniku Otvori skočni prozor - Podijeli + Dijeli Preuzimanje - Pretraživanje + Pretraga Postavke - Jeste li mislili „%1$s”\? - Podijeli pomoću - Koristi vanjski reproduktor videozapisa - Uklanja zvuk pri nekim rezolucijama - Koristi vanjski reproduktor za zvuk + Misliš li „%1$s”\? + Dijeli s + Koristi vanjski video player + Uklanja audiosnimku pri nekim rezolucijama + Koristi vanjski audio player Pretplati se Pretplaćeno - Pretplata na kanalu otkazana - Nije moguće promijeniti pretplatu - Nije moguće osvježiti pretplatu + Pretplata na kanal otkazana + Nije bilo moguće promijeniti pretplatu + Nije bilo moguće aktualizirati pretplatu Pretplate Što je novo Pozadina Skočni prozor Mapa za preuzimanje videozapisa - Preuzeti videozapisi spremaju se ovdje - Odaberi mapu za preuzimanje videozapisa - Mapa za preuzimanje zvuka - Preuzete audio-datoteke spremaju se ovdje - Odaberi mapu za preuzimanje audio-datoteka - Standardna rezolucija - Standardna rezolucija skočnog prozora - Prikaži više rezolucije - Samo neki uređaji podržavaju reprodukciju 2K/4K videozapisa + Preuzete video datoteke se spremaju ovdje + Odaberi mapu za preuzimanje video datoteka + Mapa za preuzimanje audiosnimaka + Preuzete datoteke audiosnimaka se spremaju ovdje + Odaberi mapu za preuzimanje datoteka audiosnimaka + Zadana rezolucija + Zadana rezolucija skočnog prozora + Prikaži veće rezolucije + Samo neki uređaji podržavaju reprodukciju 2K/4K videa Reproduciraj s Kodijem Instalirati nedostajući Kore program\? - Prikaži opciju \"Reproduciraj putem Kodija\" + Prikaži opciju „Reproduciraj pomoću Kodija” Prikaži opciju za reproduciranje videozapisa putem Kodija - Zvuk - Zadani format zvuka - Zadani format videozapisa + Audiosnimka + Zadani audio format + Zadani video format Tema Svijetla Tamna Crna Zapamti veličinu i poziciju skočnog prozora Zapamti posljednju veličinu i poziciju skočnog prozora - Prijedlozi pri traženju + Prijedlozi pretrage Odaberi prijedloge koji se prikazuju pri traženju Povijest pretraživanja - Svaku pretragu spremi lokalno - Pregledna Povijest - Spremaj povijest gledanja + Spremi pretrage lokalno + Povijest gledanja + Prati gledana videa Nastavi reprodukciju Nastavi reproducirati nakon prekidanja (npr. telefonski pozivi) Preuzmi - Prikaži \'Sljedeće\' i \'Slične\' videozapise + Prikaži videa „Sljedeći” i „Slični” URL nije podržan Zadani jezik sadržaja Video i audio @@ -76,11 +76,11 @@ Najbolja rezolucija Greška Greška u mreži - Nije moguće učitati sve ikone - Nije moguće dešifrirati URL potpis videozapisa - Nije moguće dohvatiti stranicu + Nije bilo moguće učitati sve sličice + Nije bilo moguće dešifrirati URL potpis videozapisa + Nije bilo moguće obraditi stranicu Sadržaj nije dostupan - Nije moguće postaviti izbornik za preuzimanje + Nije bilo moguće postaviti izbornik za preuzimanje Program/korisničko sučelje su preknuli raditi Oprosti, ovo se nije trebalo dogoditi. Prijavi pogrešku putem e-maila @@ -89,14 +89,14 @@ Informacije: Što se dogodilo: Što:\\nZahtjev:\\nJezik sadržaja:\\nZemlja sadržaja:\\nJezik programa:\\nUsluga:\\nGMT vrijeme:\\nPaket:\\nVerzija:\\nVerzija OS-a: - Vaš komentar (na engleskom): + Tvoj komentar (na engleskom): Detalji: Pokreni video, trajanje: - Profilna slika prenositelja - Goreglasovi - Doljeglasovi - Videozapis - Zvuk + Sličica avatara prenositelja + Sviđanja + Nesviđanja + Video + Audio Pokušaj ponovo tis. mil @@ -104,26 +104,26 @@ Počni Pauziraj Izbriši - Kontrolna suma + Kontrolni zbroj U redu Naziv datoteke - Niti + Komponente procesa Greška - NewPipe preuzima - Dodirni za detalje - Molimo pričekajte… + NewPipe preuzimanje + Dodirni za prikaz detalja + Pričekaj … Kopirano u međuspremnik - Molimo kasnije u postavkama odaberite mapu za preuzimanje + Odaberi mapu za preuzimanje kasnije u postavkama Ova dozvola je potrebna za \notvaranje skočnog prozora reCAPTCHA zadatak - Traži se reCAPTCHA zadatak + Zatražen je reCAPTCHA zadatak Preuzimanja Dozvoljeni znakovi u nazivima datoteka Nedozvoljeni znakovi su zamjenjeni ovima Znak za zamjenu Slova i brojevi - Posebni znakovi + Najviše posebnih znakova O NewPipeu Licence treće strane © %1$s od %2$s pod %3$s @@ -131,15 +131,15 @@ Licence Slobodan i mali YouTube program za Android. Pogledaj na GitHubu - Licenca za NewPipe + NewPipe licenca Ako imate ideja za prijevod, promjene u dizajnu, čišćenje koda ili neke veće promjene u kodu, pomoć je uvijek dobro došla. Što više radimo, to bolji postajemo! Pročitaj licencu Doprinos Povijest Povijest NewPipe obavijest - Obavijesti za NewPipe reproduktore - Reproduktor + Obavijesti za NewPipe playera + Player Ponašanje Povijest i predmemorija Poništi @@ -155,70 +155,70 @@ %s pregled %s pregleda - %s pregledi + %s pregleda - Nema videozapisa + Nema videa - %s videozapis - %s videozapisa - %s videozapisa + %s video + %s videa + %s videa Reproduciraj sve - Nije moguće reproducirati ovaj stream - Dogodila se neoporavljiva greška reproduktora - Oporavljanje od greške reproduktora + Nije bilo moguće reproducirati ovaj stream + Dogodila se neoporavljiva greška playera + Oporavljanje od greške playera Prikaži savjet za držanje - Prikaži savjet kad se pritisne gumb za pozadinsku ili skočni prozor u detaljima videa: - Želite li izbrisati ovu stavku iz povijesti pretraživanja? + Prikaži savjet kad se pritisne gumb za pozadinu ili skočni gumb u videu „Detalji:” + Želiš li izbrisati ovu stavku iz povijesti pretrage\? Sadržaj Prazna stranica - Kiosk stranica - Kanal - Odaberite kanal - Niste pretplaćeni na nijedan kanal - Odaberite kiosk + Stranica kioska + Stranica kanala + Odaberi kanal + Još nema pretplata na nijedan kanal + Odaberi jedan kiosk U trendu - Vrh 50 - Novo i popularno + 50 najboljih + Novi i popularni Ukloni Detalji - Postavke zvuka + Postavke za audiosnimke Drži pritisnuto za dodavanje u popis izvođenja [Nepoznato] Doniraj - Web stranica + Web-stranica Započni reprodukciju u pozadini Reproduciraj u skočnom prozoru Otvori ladicu Zatvori ladicu - Video reproduktor - Pozadinski reproduktor - Skočni reproduktor + Video player + Pozadinski player + Skočni player Uvjek pitaj Dohvaćanje podataka … - Učitava se odabrani sadržaj + Učitavanje traženog sadržaja Nova playlista Preimenuj Ime Dodaj u playlistu - Postavi kao minijaturu playliste + Postavi kao sličicu playliste Zabilježi playlistu Ukloni zabilješku Izbrisati ovu playlistu\? Playlista je stvorena - Dodano kao playlistu - Minijatura playliste se promijenila. + Dodano u playlistu + Sličica playliste je promijenjena. Bez titlova - Popuni - Ispuniti - Povećaj - Auto generirano + Prilagodi + Ispuni + Zumiraj + Automatski generirani Praćenje curenja memorije može uzrokovati greške u radu programa prilikom odlaganje gomile Izvijesti o krajevima životnog ciklusa Prikaži informacije - Zabilježeni popisi + Zabilježene playliste Dodaj u - Učitaj slike + Učitaj sličice Slikovna predmemorija obrisana Izbriši metapodatke iz predmemorije Kanali @@ -233,7 +233,7 @@ Prijeđi na glavni Uvezi bazu podataka Izvezi bazu podataka - Poništava vašu trenutačnu povijest, pretplate, playliste i (opcionalno) postavke + Poništava tvoju trenutačnu povijest, pretplate, playliste i (opcionalno) postavke Izvezi povijest, pretplate, playliste i postavke Izbriši povijest gledanja Briše povijest reproduciranih streamova i pozicije reprodukcije @@ -245,29 +245,29 @@ Nema takve mape Naziv datoteke ne može biti prazan Dogodila se greška: %1$s - Povucite za promjenu redoslijeda + Povuci za promjenu redoslijeda Stvori Odbaci Preimenuj 1 stavka izbrisana. Nijedan program nije instaliran za reprodukciju ove datoteke Vrati - Posjetite web stranicu NewPipe za više informacija i vijesti. - NewPipeova pravila o privatnosti - Pročitajte pravila o privatnosti + Posjeti NewPipe web-stranicu za više informacija i vijesti. + NewPipe pravila o privatnosti + Pročitaj pravila o privatnosti Zadnje svirano - Najviše svirano + Najviše reproducirano Izvezeno Uvezeno Nema važeće ZIP datoteke Upozorenje: Nije moguće uvesti sve datoteke. - Ovo će poništiti vaše trenutne postavke. - Želite li također uvesti postavke? + Ovo će prepisati tvoje trenutačne postavke. + Želiš li također uvesti postavke\? Uvoz Uvoz iz Izvoz u - Uvoz… - Izvoz… + Uvoz … + Izvoz … Uvoz datoteke Prethodni izvoz Nije bilo moguće uvesti pretplate @@ -277,14 +277,14 @@ \n1. Idi na ovaj URL: %1$s \n2. Prijavi se \n3. Pritisni „Uključeni svi podaci”, zatim „Poništi odabir svih”, a zatim odaberi samo „pretplate” i pritisni „U redu” -\n4. Pritisni na „Nastavi”, a zatim „Stvori izvoz” -\n5. Pritisni na „Preuzmi” -\n6. Dolje pritisni na UVEZI DATOEKU i odaberi .zip datoteku za peuzimanje -\n7. [Ako uvoz .zip datoteke ne uspije] Izdvoji .csv datoteku (pod \"YouTube and YouTube Music/subscriptions/subscriptions.json\"). Dolje pritisni UVEZI DATOTEKU i odaberi izdvojenu csv datoteku - vašID, soundcloud.com/vašID - Uzmite u obzir da ova operacija može uzrokovat veliku potrošnju prometa. +\n4. Pritisni „Sljedeći korak”, a zatim „Stvori izvoz” +\n5. Pritisni gumb „Preuzmi” nakon što se pojavi +\n6. Dolje pritisni UVEZI DATOEKU i odaberi preuzetu .zip datoteku +\n7. [Ako uvoz .zip datoteke ne uspije] izdvoji .csv datoteku (pod „YouTube and YouTube Music/subscriptions/subscriptions.json”), dolje pritisni UVEZI DATOTEKU i odaberi izdvojenu csv datoteku + tvojID, soundcloud.com/tvojid + Ova operacija može prouzročiti veliku potrošnju mrežnog prometa. \n -\nŽelite li nastaviti? +\nŽeliš li nastaviti\? Kontrole brzine reprodukcije Premotaj naprijed tijekom šutnje Korak @@ -294,19 +294,19 @@ Bez ograničenja Ograniči rezoluciju tijekom korištenja mobilnih podataka Nijedan - Reproduktor za stream nije pronađen (možeš instalirati VLC za reprodukciju). + Nije pronađen nijedan player streamova (možeš instalirati VLC za reprodukciju). Preuzmi datoteku streama Koristi brzo netočno premotavanje - Netočno premotavanje omogućava reproduktoru da premota brže uz manju točnost. Premotavanje od 5, 15 ili 25 sekundi s ovime nije moguće + Netočno premotavanje omogućuje playeru brže premotavanje uz manju točnost. Premotavanje od 5, 15 ili 25 sekundi s ovime ne radi Otkaži pretplatu Odaberi karticu - Ažuriranja + Aktualiziranja Događaji Datoteka obrisana Obavijest za nove NewPipe verzije Briše povijest ključnih riječi pretraživanja Vanjska pohrana nije dostupna - Ažuriranja + Aktualiziranja Prikaži obavijest i zatraži aktualiziranje programa kad je dostupna nova verzija Popis Popločeno @@ -314,51 +314,51 @@ Dodirni za preuzimanje Preuzimanje nije uspjelo Prikaži pogrešku - Isključi za sprječavanje učitavanja sličica, čime se štede podatci i memorija. Promjena postavke čisti predmemoriju u radnoj memoriji i u pohrani + Isključi za sprečavanje učitavanja sličica, čime se štedi korištenje podataka i memorije. Promjene čiste predmemoriju slika radne memorije i diska Izbriši sve podatke web-stranica iz predmemorije Metapodaci su izbrisani Automatski dodaj sljedeći stream u popisa izvođenja - Nastavi završavati (ne ponavljajući) popis izvođenja dodavanjem povezanog streama + Nastavi završavati (ne ponavljajući) popis reprodukcija dodavanjem povezanog streama Kontrola glasnoće pomoću gesti - Koristi geste za kontrolu glasnoće + Koristi geste za upravljanje glasnoćom playera Kontrola svjetline pomoću gesti - Koristi gesture za kontrolu svjetline + Koristi gesture za upravljanje svjetlinom playera Zadana zemlja sadržaja Otkrivanje grešaka Obavijest o novoj verziji programa Preuzimanje na vanjsku SD karticu nije moguće. Ponovo postaviti lokaciju mape za preuzimanje\? - Vanjski reproduktori ne podržavaju ove vrste veza + Vanjski playeri ne podržavaju ove vrste poveznica Nije pronađen nijedan videozapis - Nije pronađen nijedan audio zapis + Nije pronađena nijedna audiosnimka Nema takve datoteke/izvora sadržaja Datoteka ne postoji ili joj nedostaje dopuštenje za čitanje ili pisanje Nema dostupnih zapisa za preuzimanje - Neuspjelo čitanje spremljenih kartica, stoga se koriste zadane - Vratiti zadane - Želite li vratiti zadane postavke\? + Nije bilo moguće čitati spremljene kartice, stoga se koriste zadane + Obnovi standardne vrijednosti + Želiš li obnoviti standardne vrijednosti\? Broj pretplatnika nije dostupan - NewPipe razvijaju volonteri koji provode vrijeme donoseći vam najbolje iskustvo. Vratite im kako biste programerima učinili da NewPipe bude još bolji dok uživate u šalici kave. + NewPipe razvijaju volonteri koji provode vrijeme kako bi doprinijeli najboljem iskustvu. Doprinesi programerima kako bi poboljšali NewPipe dok uživaju u šalici kave. Koje su kartice prikazane na glavnoj stranici Konferencije - Željena radnja otvaranja streama + Željena radnja za otvaranje Zadana radnja pri otvaranju sadržaja — %s Titlovi - Promijeni veličinu podnaslova reproduktora i pozadinske stilove reproduktora. Za stupanje na snagu, program se mora ponovo pokrenuti - Prisilno izvješćivanje o greškama Rx-a koje se ne mogu isporučiti izvan \'fragmenta\' ili životnog ciklusa aktivnosti nakon odlaganja - Uvezite SoundCloud profil tako da upišete URL ili svoj ID: + Promijeni veličinu podnaslova i pozadinske stilove playera. Zahtijeva ponovno pokretanje programa + Prisilno izvijesti o neisporučivim Rx iznimaka izvan fragmenta ili životnog ciklusa aktivnosti nakon odlaganja + Uvezi SoundCloud profil upisom URL-a ili svog ID-a: \n -\n1. Omogućite \"način rada na radnoj površini\" u web-pregledniku (stranica nije dostupna na mobilnim uređajima) -\n2. Idite na ovaj URL: %1$s -\n3. Ulogirajte se -\n4. Kopirajte URL profila na koji ste preusmjereni. - brzina +\n1. Omogući „način rada na radnoj površini” u web-pregledniku (stranica nije dostupna na mobilnim uređajima) +\n2. Idi na ovaj URL: %1$s +\n3. Prijavi se +\n4. Kopiraj URL profila na koji te se preusmjerava. + Brzina Visina tona Odspoji (može prouzročiti izobličenje) - Sklopi prilikom mijenjanje programa - Radnja prilikom prebacivanja na drugi program iz glavnog video reproduktora – %s - Smanji na pozadinski reproduktor - Smanji na skočni reproduktor - Način prikaza popisa + Smanji prilikom mijenjanje programa + Radnja prilikom prebacivanja na drugi program iz glavnog video playera – %s + Smanji na pozadinski player + Smanji na skočni player + Način prikaza kao popis Automatski Gotovo Na čekanju @@ -367,18 +367,18 @@ naknadna obrada Popis izvođenja Sustav je odbio radnju - Generirajte jedinstveni naziv + Generiraj jedinstveni naziv Prepiši Datoteka s tim nazivom već postoji Preuzeta datoteka s tim nazivom već postoji Datoteka s ovim nazivom se već preuzima Odredišna mapa ne može biti stvorena Datoteka se ne može stvoriti - Nije moguće uspostaviti sigurnu vezu - Nije moguće pronaći server + Nije bilo moguće uspostaviti sigurnu vezu + Nije bilo moguće pronaći server Nije moguće povezati se s serverom Server ne šalje podatke - Poslužitelj ne prihvaća preuzimanja s više niti, pokušaj ponovo s @string/msg_threads = 1 + Poslužitelj ne prihvaća preuzimanja višestrukih procesa, pokušaj ponovo s @string/msg_threads = 1 Nije pronađeno Naknadna obrada nije uspjela Stop @@ -390,23 +390,23 @@ Isključi, kako bi se komentari sakrili Automatska reprodukcija Nema komentara - Komentare nije moguće učitati + Nije bilo moguće učitati komentare Zatvori - NewPipe je copyleft libre software: Može se koristiti, proučavati i poboljšavati po volji. Konkretno, može se redistribuirati i / ili modificirati pod uvjetima GNU opće javne licence koju je objavila Free Software Foundation, bilo licence verzije 3, ili (po vlastitom izboru) bilo koje kasnije verzije. + NewPipe je copyleft libre softver: Može se koristiti, proučavati i poboljšavati po volji. Konkretno, može se redistribuirati i / ili modificirati pod uvjetima GNU opće javne licence koju je objavila zaklada Free Software Foundation, pod verzijom 3 licence, ili (po vlastitom izboru) bilo koje kasnije verzije. Projekt NewPipe ozbiljno shvaća tvou privatnost. Stoga program ne prikuplja nikakve podatke bez tvog pristanka. \nNewPipe pravila o privatnosti detaljno objašnjavaju koji se podaci šalju i spremaju kad šalješ izvještaje o prekidu rada programa. - Kako bismo se uskladili s Europskom općom uredbom o zaštiti podataka (GDPR), upozoravamo vas na politiku privatnosti tvrtke NewPipe. Pažljivo ga pročitajte. -\nZa slanje izvješća o pogreškama potrebno je prihvatiti politiku privatnosti. + Kako bismo se uskladili s Europskom općom uredbom o zaštiti podataka (GDPR), ovime upozoravamo na NewPipe politiku privatnosti. Pažljivo je pročitaj. +\nZa slanje izvješća o pogreškama moraš prihvatiti politiku privatnosti. Nastavi reprodukciju - Vrati zadnji položaj reprodukcije + Obnovi zadnji položaj reprodukcije Pozicije na popisima - Prikaži poziciju reprodukcije na listi + Prikaži poziciju reprodukcije u popisima Obriši podatke Pozicije reprodukcije su izbrisane Datoteka je premještena ili izbrisana Datoteka s ovim nazivom već čeka na preuzimanje Vrijeme povezanosti je isteklo - Želite li očistiti povijest preuzimanja ili izbrisati sve preuzete datoteke\? + Želiš li izbrisati povijest preuzimanja ili izbrisati sve preuzete datoteke\? Započni preuzimanja Zaustavi preuzimanja Pitaj gdje preuzeti @@ -416,50 +416,50 @@ Nitko ne gleda Nitko ne sluša Jezik će se promijeniti nakon ponovnog pokretanja programa - Zadani Kiosk + Standardni kiosk Podržani su samo HTTP URL-ovi - Lokalno - Nedavno dodano - Autogenerirano (prenositelj nedefiniran) + Lokalni + Nedavno dodani + Automatski generirano (prenositelj nedefiniran) Očisti povijest preuzimanja Izbriši preuzete datoteke Dopusti prikaz iznad drugih programa Jezik programa Zadani sustav - Videozapisi - Isključi + Videa + Isključi zvuk Uključi Pomoć - Učitavanje feeda… - Želite li izbrisati ovu grupu\? - Novi - Uvijek ažuriraj + Učitavanje feeda … + Želiš li izbrisati ovu grupu\? + Nova + Uvijek aktualiziraj Omogući brz način Onemogući brz način Memorija uređaja je popunjena - Najomiljenije - Pritisnite \"Gotovo\" kad riješeno + Najomiljeniji + Pritisni „Gotovo” kad je riješeno Gotovo - ∞ videozapisa - 100+ videozapisa - Prijavite grešku na GitHub + ∞ videa + Više od 100 videa + Prijavi grešku na GitHub-u Umjetnici Albumi Pjesme - Napravio %s + Stvoren od %s Nikada Ograniči popis preuzimanja Koristi birač mapa sustava (SAF) Ukloni pregledano - Ukloni pogledane videozapise\? + Ukloni pogledana videa\? %d sekunda - %d sekundi + %d sekunde %d sekundi - %d minutu - %d minuta + %d minuta + %d minute %d minuta @@ -469,7 +469,7 @@ %d sat - %d sati + %d sata %d sati Nije učitano: %d @@ -481,63 +481,63 @@ Gumb druge radnje Gumb prve radnje Prikazuju se rezultati za: %s - Nije moguće prepoznati URL. Želiš li otvoriti s drugim programom\? - Smanjiti omjer minijatura na 1:1 + Nije bilo moguće prepoznati URL. Želiš li otvoriti s drugim programom\? + Odreži sličicu na omjer 1:1 Učitavanje u predmemoriju Istovremeno se pokreće jedno preuzimanje Dodano u popis izvođenja Dodaj u popis izvođenja - Popis izvođenja + Reproduciraj popis izvođenja Automatski popis izvođenja - Popis izvođenja aktivnog reproduktora će se zamijeniti - Prebacivanje s jednog reproduktora na drugi može zamijeniti popisa izvođenja + Popis izvođenja aktivnog playera će se zamijeniti + Prebacivanje s jednog playera na drugi može zamijeniti tvoj popis izvođenja Pitaj prije pražnjenja popisa izvođenja %s slušatelj %s slušatelja %s slušatelja - datoteka ne može biti prepisana - Promijeni omjer prikazane minijature videa u obavijesti iz 16:9 na 1:1 (može prouzročiti izobličenja) + datoteka se ne može prepisato + Odreži prikazane sličice videa u obavijesti iz omjera 16:9 na 1:1 U kompaktnom prikazu obavijesti mogu se odabrati najviše 3 radnje! Od %s - Minijatura avatara kanala + Sličica avatara kanala Dohvati iz određenog feeda kad je dostupno Vrijeme nakon zadnjeg aktualiziranja prije nego što se pretplata smatra zastarjelom – %s - Prag aktualiziranja feedova + Prag za aktualiziranje feedova Feed Prikaži samo negrupirane pretplate Prazno ime grupe - %d odabrani - %d odabrana + %d odabrana + %d odabrane %d odabranih - Obrada feeda … + Obrada feeda u tijeku … Zadnje aktualiziranje feeda: %s Grupe kanala - Da, i djelomično pogledane videozapise - Odaberi primjerak + Da, i djelomično pogledana videa + Odaberi jednu instancu Aplikacija će te pitati kamo spremati preuzimanja. \nOmogući birač mapa sustava (SAF), ako želiš preuzimati na vanjsku SD karticu Nije moguće obnoviti ovo preuzimanje Napredak je izgubljen, jer je datoteka izbrisana NewPipe se zatvorio tijekom rada s datotekom Stranica playliste - Videzapisi koji su gledani prije i nakon dodavanja u playlistu će se ukloniti. + Videa koji su gledani prije i nakon dodavanja u playlistu će se ukloniti. \nStvarno ih želiš ukloniti\? Ovo je nepovratna radnja! Još nema zabilježenih playlista Odaberi playlistu obnavljanje Samo na Wi-Fi mreži - Pokreni automatski – %s + Pokreni reprodukciju automatski – %s Prikaži curenje memorije %s gledatelj %s gledatelja %s gledatelja - Uklj/Isklj uslugu, trenutačno odabrana: + Uključi/isključi uslugu, trenutačno odabrana: Kopiraj formatirani izveštaj Izbriši kolačiće koje NewPipe sprema nakon rješavanja reCAPTCHA reCAPTCHA kolačići su izbrisani @@ -548,51 +548,51 @@ YouTube nudi postavku „Ograničeni način rada”, čime se skriva sadržaj za odrasle Uključi YouTube postavku „Ograničeni način rada” Prikaži sadržaj koji vjerojatno nije prikladan za djecu, jer je dobno ograničen (kategorija 18) - Primjerak već postoji - Neuspjela provjera primjerka - Upiši URL primjerka - Dodaj primjerak - Pronađi omiljene primjerke na %s - Odaberi tvoje omiljene PeerTube primjerke - PeerTube primjerci + Instanca već postoji + Nije bilo moguće provjeriti instancu + Upiši URL instance + Dodaj instancu + Pronađi instance koje voliš na %s + Odaberi svoje omiljene PeerTube instance + PeerTube instance Vrijeme premotavanja prema naprijed ili natrag Ništa Promiješaj Ponovi - Provjeri je li tvoj problem već postoji. Dupla pojava problema krade nam vrijeme koje bismo mogli utrošiti na ispravljanje same greške. + Provjeri je li problem već postoji. Prijavljivanje istog već prijavljenog problema krade nam vrijeme koje bismo mogli utrošiti na ispravljanje greške. Za uređivanje radnji u obavijestima, dodirni ih. Označi do tri radnje za prikaz u kompaktnoj obavijesti koristeći oznake na desnoj strani - Zbog ograničenja ExoPlayera, trajanje traženja postavljeno je na %d s - Neka Android prilagodi boju obavijesti prema glavnoj boji minijature (ovo nije dostupno na svim uređajima) + Zbog ograničenja ExoPlayera, trajanje premotavanja postavljeno je na %d s + Neka Android prilagodi boju obavijesti prema glavnoj boji sličice (ovo nije dostupno na svim uređajima) Oboji obavijest NewPipe još ne podržava ovaj sadržaj. \n \nNadamo se da će biti podržan u budućoj verziji. - Prikaži minijaturu kao pozadinu pri zaključanom ekranu i unutar obavijesti - Prikaži minijaturu + Koristi sličicu za pozadinu zaključanog ekrana i za obavijesti + Prikaži sličicu Prikaži izvorno vrijeme elemenata - „Okvir za pristup spremištu” omogućuje preuzimanje na SD karticu + „Storage Access Framework” omogućuje preuzimanje na SD karticu Izvorni tekstovi usluga bit će vidljivi u elementima prijenosa Dostupno je u nekim uslugama. Obično je puno brže, ali može dohvatiti ograničenu količinu stavki i često nepotpune podatke (npr. bez trajanja, vrste stavke, bez stanja uživo) - Mislite li da je učitavanje feeda prespor\? Ako je to slučaj, pokušajte omogućiti brzo učitavanje (možete ga promijeniti u postavkama ili pritiskom na donji gumb). + Misliš da je učitavanje feeda presporo\? Ako da, pokušaj omogućiti brzo učitavanje (možeš ga promijeniti u postavkama ili pritiskom na donji gumb). \n -\nNewPipe nudi dvije strategije ulaganja feeda: -\n• Dohvaćanje cijelog pretplatničkog kanala, koji je spor, ali cjelovit. -\n• Korištenje namjenske krajnje točke usluge, koja je brza, ali obično nije potpuna. +\nNewPipe nudi dvije strategije učitavanja feeda: +\n• Dohvaćanje cijelog pretplatničkog kanala, što je sporo, ali cjelovito. +\n• Korištenje namjenske krajnje točke usluge, što je brzo, ali obično nepotpuno. \n -\nRazlika je u tome što brzom obično nedostaju neke informacije, poput trajanja ili vrste stavke (ne može razlikovati videozapise uživo od uobičajenih), a možda će vratiti i manje predmeta. +\nRazlika je u tome što brzom načinu obično nedostaju neke informacije, poput trajanja ili vrste predmeta (ne može razlikovati videa uživo od običnih videa), a možda će vratiti i manje predmeta. \n \nYouTube je primjer usluge koja nudi ovaj brzi način sa svojim RSS feedom. \n -\nDakle, izbor se svodi na ono što više volite: brzinu ili precizne informacije. +\nDakle, izbor se svodi na ono što više voliš: brzinu ili precizne informacije. Izračunavanje šifriranja Obavijest šifriranja videa Obavijesti o napretku šifriranja videa Nedavni Isključi za skrivanje polja metapodataka s dodatnim podacima o autoru streama, sadržaju streama ili zahtjevu za pretraživanje Prikaži metapodatke - Slični videozapisi + Povezani predmeti Nijedan program na tvom uređaju ovo ne može otvoriti - Poglavlja videozapisa + Poglavlja Opis Komentari Isključi za skrivanje opisa videozapisa i dodatnih informacija @@ -620,12 +620,12 @@ Isklj. Uklj. Način rada na tabletu - Otvori stranicu - Srce autora + Otvori web-stranicu + Od autora obilježeno srcem Privatno Nenavedeno Javno - URL minijature + URL sličice Poslužitelj Podrška Jezik @@ -633,42 +633,41 @@ Licenca Oznake Kategorija - Onemogućite odabir teksta u opisu - Omogućite odabir teksta u opisu + Onemogući biranje teksta u opisu + Omogući biranje teksta u opisu Račun ukinut Prikaži pogledane stavke Autorov račun je ukinut. -\nNewPipe ubuduće neće moći učitavati ovaj feed. -\nŽelite li otkazati pretplatu na ovaj kanal\? - Nije moguće učitati feed za \'%s\'. +\nNewPipe ubuduće neće moći učitavati ovaj feed. +\nŽeliš li otkazati pretplatu na ovaj kanal\? + Nije bilo moguće učitati feed za „%s”. Pogreška pri učitavanju feeda - Počevši od Androida 10, podržan je samo \'Storage Access Framework\' - „Storage Access Framework“ nije podržan na Androidu KitKat i starijim + Počevši od Androida 10, podržano je samo radno okruženje „Storage Access Framework” Od vas će se tražiti gdje spremiti svako preuzimanje Ne prikazuj - Niska kvaliteta (manji) + Niska kvaliteta (manja) Visoka kvaliteta (veća) - Pregled sličica trake za pretraživanje - Mapa za preuzimanje još nije postavljena, odaberite zadanu mapu za preuzimanje + Pregled sličica premotavanja + Mapa za preuzimanje još nije postavljena, odaberi standardnu mapu za preuzimanje Komentari su onemogućeni Označi kao pogledano Način rada brzog feeda ne pruža više informacija o ovome. Interno Privatnost 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. - %s daje ovaj razlog: - Obrada... Pričekajte trenutak - Povucite stavke da biste ih uklonili + %s pruža ovaj razlog: + Obrada u tijeku … Može malo potrajati + Za ukljanjanje stavki povuci ih Prikazati indikatore slike - Dovršeno %s preuzimanje - Dovršena %s preuzimanja - Dovršeno %s preuzimanja + Preuzimanje je gotovo + %s preuzimanja su gotova + %s preuzimanja su gotova - Pokreni glavni reproduktor u cjeloekranskom prikazu - Reproduciraj sljedeće - U redu čekanja - Prikažite Picassove vrpce u boji na slikama koje označavaju njihov izvor: crvena za mrežu, plava za disk i zelena za memoriju + Pokreni glavni player u cjeloekranskom prikazu + Reproduciraj sljedeći + Sljedeći u popisu izvođenja + Prikaži Picassove vrpce u boji na slikama koje označavaju njihov izvor: crvena za mrežu, plava za disk i zelena za memoriju Izbrisano %1$s preuzimanje Izbrisana %1$s preuzimanja @@ -679,37 +678,59 @@ Traženje novih verzija … Prijedlozi lokalne pretrage Traži nove verzije - Nemoj pokretati videa u mini reproduktoru, već se izravno pokreni cjeloekranski prikaz, ako je automatsko okretanje zaključano. Mini reproduktoru i dalje možeš pristupiti izlaskom iz cjeloekranskog prikaza - Novi feedovi + Nemoj pokretati videa u mini playeru, već izravno pokreni cjeloekranski prikaz, ako je automatsko okretanje zaključano. Mini playeru i dalje možeš pristupiti napuštanjem cjeloekranskog prikaza + Novi elementi feeda Obavijest o prijavi greške Obavijesti za prijavu grešaka Stvori obavijest o grešci NewPipe je naišao na grešku, dodirni za prijavu Došlo je do greške, pogledaj obavijest Prekini rad playera - Obavijest reproduktora - Prilagođavanje obavijesti reproduktora + Obavijest playera + Konfiguriraj obavijest trenutačno reproduciranog streama Obavijesti Novi videozapisi - Obavijesti novih streamova pretplaćenih kanala - Želite li izbrisati sve preuzete datoteke\? + Obavijesti novih streamova od pretplaćenih kanala + Želiš li izbrisati sve preuzete datoteke\? Obavijesti su onemogućene - Pretplatili ste se ovome kanalu + Pretplatio/la si se na ovaj kanal , - Uključiti/isključiti sve + Uključi/isključi sve Bilo kakva mreža Obavijesti novih streamova pretplaćenih kanala - Pokaži zalogajnicu greške - Učitavanje pojedinosti streama… - Pokrenite provjeru novih streamova - Učestalost provjere - LeakCanary nije dostupno + Prikaži kratku poruku greške + Učitavanje pojedinosti streama … + Pokreni traženje novih streamova + Prvjeravanje učestalosti + Biblioteka „LeakCanary” nije dostupna Podešavanje visine tona po glazbenim polutonovima Obavijesti o novim streamovima Potrebna mrežna veza Zadano za ExoPlayer - Primite obavijesti - Za ovu radnju nije pronađen odgovarajući upravitelj datoteka. -\nMolimo vas da instalirate upravitelj za datoteke ili da pokušate onemogućiti \'%s\' u postavkama preuzimanja. + Primaj obavijesti + Za ovu radnju nije pronađen odgovarajući upravljač datoteka. +\nInstaliraj upravljač datoteka ili pokušaj onemogućiti „%s” u postavkama preuzimanja. Prikvačeni komentar + Prikazuje opciju prekida rada kad se player koristi + Prikaži „Prekini rad playera” + ExoPlayer standard + Posto + Poluton + Streamovi koje program za preuzimanje još ne podržava se ne prikazuju + Vanjski playeri ne podržavaju odabrani stream + Promijeni veličinu intervala učitavanja (trenutačno %s). Niža vrijednost može ubrzati početno učitavanje videa. Promjene zahtijevaju ponovno pokretanje playera. + + %s novi stream + %s nova streama + %s novih streamova + + Veličina intervala učitavanja reprodukcije + Nepoznat format + Nepoznata kvaliteta + Nijedan stream audiosnimaka nije dostupan za vanjske playere + Nijedan video stream nije dostupan za vanjske playere + Odaberi kvalitetu za vanjske playere + Prikaži buduća videa + Za ovu radnju nije pronađen odgovrajući upravljač datoteka. +\nInstaliraj „Storage Access Framework” kompatibilni upravljač datoteka. \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index e5457d3e1..49b472379 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -319,7 +319,7 @@ Eltüntetés Lejátszási lista könyvjelzőzése Egy hasonló videó hozzáadása a befejeződő (nem ismétlődő) lejátszási sorhoz - Sor + Sorba állítás a fájl nem írható felül Az előre- és visszatekerés időtartama Utolsó lejátszási pozíció visszaállítása @@ -362,11 +362,11 @@ Nincs talalat A kiszolgáló nem fogad többszálú letöltést, próbálkozzon újra ezzel: @string/msg_threads = 1 A kiszolgáló nem küld adatokat - Nem lehet csatlakozni a kiszolgálóhoz + A kiszolgáló szerver nem elérhető A kiszolgáló nem található Nem sikerült biztonságos kapcsolatot létesíteni A célmappa nem hozható létre - A fájlt nem lehet létrehozni + A fájlt nem sikerült létrehozni Hiba megjelenítése Ezzel a névvel egy letöltés már várakozik Ezzel a névvel egy letöltés már folyamatban van @@ -582,7 +582,6 @@ Tiltsa le a médiacsatornázást, ha fekete képernyőt vagy akadozást tapasztal videólejátszáskor Picasso színes szalagok megjelenítése a képek fölött, megjelölve a forrásukat: piros a hálózathoz, kék a lemezhez, zöld a memóriához Minden letöltésnél meg fogja kérdezni, hogy hova mentse el - A „Storage Access Framework” nem támogatott Android KitKaten vagy régebbin Válasszon egy példányt Lista legutóbbi frissítése: %s Lista betöltése… @@ -684,4 +683,39 @@ Rögzített megjegyzés LeakCanary nem elérhető + Lejátszó értesítés + Módosítsa a betöltési intervallum méretét (jelenleg %s). Az alacsonyabb érték felgyorsíthatja a videó kezdeti betöltését. A változtatásokhoz a lejátszó újraindítása szükséges. + Az aktuális lejátszás konfigurálása értesítés + Értesítések + Új élő közvetítések + Értesítések új élő közvetítésekről a feliratkozott csatornák esetén + Élő közvetítés betöltése.… + Keressen új élő közvetítést + Új élő közvetítés értesítések + Értesítésen új élő közvetítés esetén a feliratkozott csatornákhoz + Ellenőrzési gyakoriság + Szükséges hálózati kapcsolat + Bármilyen hálózat + Törli az összes letöltött fájlt a lemezről\? + Értesítsen + Értesítéstek kikapcsolva + Lejátszási intervallum mérete + Százaléka + + %s új elő közvetítés + %s új elő közvetítések + + ExoPlayer alapértelmezett + Feliratkoztál erre a csatornára + , + Azok az élő adások melyek nem támogatottak a letöltő által, rejtve vannak. + A választott élő adást nem lehet külső lejátszóval lejátszani. + Összes váltása + Külső lejátszók számára nem érhető el az hang csatorna + Külső lejátszók számára nem érhető el videó + Válassz minőséget külső lejátszókhoz + Ismeretlen formátum + Ismeretlen minőség + Félhang + Jövőbeli videók megjelenítése \ No newline at end of file diff --git a/app/src/main/res/values-hy/strings.xml b/app/src/main/res/values-hy/strings.xml index 10d397aef..218854841 100644 --- a/app/src/main/res/values-hy/strings.xml +++ b/app/src/main/res/values-hy/strings.xml @@ -217,4 +217,7 @@ Հանրային Պիտակներ Ծանուցումները անջատված են + Ոչինչ բացի դատարկությունից + Կրկին փորձել + Հոսքի նորերը \ No newline at end of file diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index d5bc90e6f..f4383defb 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -233,4 +233,16 @@ Solmente alicun apparatos pote reproducer videos 2K/4K Initiar le reproductor principal in schermo plen Solmente le URLs HTTPS es supportate + Repeter + Aleatori + Cargante fe + Privacitate + Licentia + Comenciava le discarga + Private + Aperir le sito web + Per %s + Monstrar le videos futur + Radio + Create per %s \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index c1f296849..f7fb4113a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -121,7 +121,7 @@ Terlepas apakah Anda memiliki ide untuk; terjemahan, perubahan desain, pembersihan kode, atau perubahan kode yang signifikan, segala bantuan akan selalu diterima. Semakin banyak akan semakin baik jadinya! Baca lisensi Kontribusi - Subscribe + Berlangganan Disubscribe Apa Yang Baru Lanjutkan pemutaran @@ -550,7 +550,7 @@ Tombol tindakan ketiga Tombol tindakan kedua Tombol tindakan pertama - Ubah ukuran thumbnail yang ditampilkan di notifikasi dari rasio aspek 16:9 ke 1:1 (mungkin terdistorsi) + Ubah ukuran thumbnail yang ditampilkan di notifikasi dari rasio aspek 16:9 ke 1:1 Ubah ukuran thumbnail ke rasio aspek 1:1 Tampilkan kebocoran memori Ditambahkan @@ -623,7 +623,6 @@ Tidak bisa memuat langganan untuk \'%s\'. Galat memuat langganan Mulai Android 10, hanya \'Storage Access Framework\' yang didukung - \'Storage Access Framework\' tidak didukung pada Android KitKat dan yang lebih rendah Anda akan ditanya lokasi penyimpanan berkas unduhan Belum ada folder unduhan, pilih folder unduhan sekarang Nonaktif @@ -665,15 +664,15 @@ Sebuah kegalatan terjadi, lihat notifikasinya Menampilkan sebuah snackbar kegalatan Buat sebuah notifikasi kegalatan - Tidak ada manajer file yang ditemukan untuk tindakan ini. -\nMohon instal sebuah manajer file atau coba menonaktifkan \'%s\' di pengaturan unduhan. + Tidak ada pengelola berkas yang ditemukan untuk tindakan ini. +\nSilakan pasang pengelola berkas atau coba nonaktifkan \'%s\' di pengaturan unduhan NewPipe mengalami sebuah kegalatan, ketuk untuk melaporkan - Tidak ada manajer file yang ditemukan untuk tindakan ini. -\nMohon instal sebuah manajer file yang kompatibel dengan Storage Access Framework. + Tidak ada pengelola berkas yang ditemukan untuk tindakan ini. +\nSilakan pasang pengelola berkas yang kompatibel dengan Storage Access Framework Komentar dipin LeakCanary tidak tersedia Default ExoPlayer - Ubah ukuran interval pemuatan (saat ini %s). Sebuah nilai yang rendah mungkin dapat membuat pemuatan video awal lebih cepat. Membutuhkan sebuah pemulaian ulang pada pemain. + Ubah ukuran interval pemuatan (saat ini %s). Nilai yang rendah mungkin dapat membuat pemuatan video awal lebih cepat. Perubahan membutuhkan pemutar dimulai ulang Memuat detail stream… Frekuensi pemeriksaan Dibutuhkan koneksi jaringan @@ -705,4 +704,5 @@ Pilih kualitas untuk pemain eksternal Format tidak diketahui Ukuran interval pemuatan playback + Tampilkan video mendatang \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f347eba67..271d45ecb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -560,8 +560,8 @@ Casuale Niente Ripeti - Ridimensiona copertina alla proporzione 1:1 - Modifica la proporzione della copertina del video mostrata nella notifica da 16:9 a 1:1 (può introdurre distorsioni) + Ritaglia copertina con proporzione 1:1 + Ritaglia la copertina del video mostrata nella notifica, cambiando la proporzione da 16:9 a 1:1 Mostra memory leak Aggiunto alla coda Accoda @@ -633,7 +633,6 @@ Impossibile caricare feed per \"%s\". Errore caricamento feed A partire da Android 10 è supportato solo il Framework di accesso all\'archiviazione - Il Framework di accesso all\'archiviazione non è supportato su Android KitKat e versioni precedenti È necessario specificare la destinazione di ogni dowload Non è impostata alcuna cartella per i file scaricati, scegliere quella predefinita Disattivata @@ -675,17 +674,17 @@ NewPipe ha riscontrato un errore, tocca per segnalarlo Mostra un messaggio di errore Non è stato trovato alcun gestore di file appropriato per questa azione. -\nInstallane uno prova a disattivare \"%s\" nelle impostazioni di download. +\nInstallane uno prova a disattivare \"%s\" nelle impostazioni di download Notifica per segnalazione errori Notifiche per segnalare errori Si è verificato un errore, vedi la notifica Crea una notifica di errore Non è stato trovato alcun gestore di file appropriato per questa azione. -\nInstallane uno compatibile con Storage Access Framework. +\nInstallane uno compatibile con Storage Access Framework Commento in primo piano LeakCanary non è disponibile Predefinito ExoPlayer - Cambia la dimensione dell\'intervallo da caricare (attualmente %s). Un valore basso può velocizzare il caricamento iniziale del video. La modifica richiede il riavvio del lettore. + Cambia la dimensione dell\'intervallo da caricare (attualmente %s). Un valore basso può velocizzare il caricamento iniziale del video. La modifica richiede il riavvio del lettore Notifiche di nuovi contenuti dalle iscrizioni Frequenza controllo Connessione di rete richiesta @@ -717,5 +716,6 @@ Seleziona qualità per lettori esterni Qualità sconosciuta Formato sconosciuto - Dimensione dell\'intervallo di caricamento della riproduzione + Dimensione intervallo di caricamento della riproduzione + Mostra video futuri \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 955912126..223a5f21a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -14,7 +14,7 @@ 動画を保存するフォルダー ダウンロードした動画をここに保存します 動画ファイルをダウンロードするフォルダーを選択して下さい - デフォルトの解像度 + デフォルトの画質 Kodi で再生 Kore をインストールしますか? 「Kodi で再生」オプションを表示 @@ -24,7 +24,7 @@ ダウンロード 「次の動画」と「関連動画」を表示 対応していないURLです - デフォルトの言語 + デフォルトの言語設定 動画と音声 ビデオ再生、時間: 投稿者アイコンのサムネイル @@ -49,8 +49,8 @@ 保存メニューを設定できませんでした コンテンツ 年齢制限のあるコンテンツを表示 - 申し訳ありません。発生すべきでものではありませんでした。 - メールで不具合を報告 + 申し訳ありません。想定外のエラーが発生しました。 + 不具合をメールで報告 申し訳ありません、不具合が発生しました。 報告 情報: @@ -70,15 +70,15 @@ ファイル名 同時接続数 エラー - NewPipe ダウンロード中 + ダウンロード中 (NewPipe) タップして詳細を表示 お待ちください… クリップボードにコピーしました - 後ほど設定でダウンロードフォルダを定義してください + 後ほど、ダウンロードフォルダーを設定してください ダウンロード ダウンロード 不具合報告 - アプリ/UI がクラッシュしました + アプリ(UI)がクラッシュしました どんな問題:\\nリクエスト:\\nコンテンツ言語:\\nコンテンツ国:\\nアプリ言語:\\nサービス:\\nGMT 時間:\\nパッケージ:\\nバージョン:\\nOSバージョン: reCAPTCHA の要求 reCAPTCHA を要求しました @@ -93,18 +93,18 @@ ポップアップモードで再生中 無効 デフォルトの動画形式 - デフォルトのポップアップ解像度 - より高い解像度を表示 - 一部のデバイスのみ2K/4K動画を再生できます + デフォルトの画質 (ポップアップ表示) + より高い画質を表示 + 2K/4K動画は一部のデバイスでのみ再生できます バックグラウンド ポップアップ 消去 - ポップアップの属性を記憶 - ポップアップしたサイズと位置を記憶します + ポップアップの属性を記憶する + ポップアップのサイズと位置を記憶する 一部の解像度では音声がありません 検索候補の表示 検索時に表示する候補を選択します - 最高の解像度 + 最高 NewPipe について サードパーティー ライセンス © %1$s 作者 %2$s ライセンス %3$s @@ -125,8 +125,8 @@ 新着 検索履歴 検索履歴を記憶します - 視聴履歴 - 再生した履歴を記憶します + 再生履歴 + 再生履歴を記憶します 再生の再開 電話などによる中断の後、再生を再開します プレイヤー @@ -138,9 +138,9 @@ NewPipe の通知 [不明] 動画の再生ができませんでした - 回復不能な再生エラーが発生しました - 何も見つかりませんでした - チャンネル登録なし + 回復不能なエラーが発生しました + 一致する結果はありませんでした + チャンネル登録者なし 動画がありません 保存 ファイル名に使用可能な文字 @@ -178,7 +178,7 @@ データベースをエクスポート 既存の履歴、登録リスト、プレイリストおよび (任意) 設定は上書きされます 再生履歴、登録チャンネル一覧、プレイリストおよび設定をエクスポートします - 再生エラーからの回復中 + エラーから回復中です 外部プレイヤーは、これらのタイプのリンクをサポートしていません エクスポートしました インポートしました @@ -201,7 +201,7 @@ キャッシュを消去 アプリ内のキャッシュデータをすべて削除します キャッシュが消去されました - 次のを自動でキューに追加する + 次の動画を自動でキューに追加する デバッグ ファイル 動画が見つかりません @@ -255,7 +255,7 @@ プライバシーポリシーを確認 おおまかなシーク おおまかなシークを使用することで精度が下がる代わりに高速にシークができます。5 秒、15 秒または 25 秒間隔のシークはできません - すべてのサムネイルの読み込みと保存を無効化します。このオプションを切り替えるとメモリおよびディスク上の画像キャッシュが消去されます + サムネイルの読み込みと保存を無効化します。(このオプションを切り替えるとメモリとディスク上の画像キャッシュが消去されます) キューに関連動画を追加して再生を続ける (繰り返ししない場合) すべての再生履歴を削除しますか? すべての検索履歴を削除しますか? @@ -276,11 +276,11 @@ 最も再生された動画 拡大 プレイリスト - 「長押ししてキュー」のヒントを表示 + 「長押しでキューに追加」のヒントを表示 トラック NewPipe のプレイヤーの通知 新着と人気 - 長押ししてキューに追加 + 長押しでキューに追加 ポップアップで連続再生を開始 お好みの「開く」アクション コンテンツを開くときのデフォルト動作 — %s @@ -302,7 +302,7 @@ 同意する 拒否する 制限なし - モバイルデータ使用時の解像度の制限 + モバイルネットワーク使用時の画質 アプリ切り替え時の最小化 プレイヤーから他のアプリに切り替え時の動作 — %s 何もしない @@ -345,7 +345,7 @@ NewPipe のアップデートがあります! タップでダウンロード 完了 - 保留中 + 順番に処理中 一時停止 順番待ちに追加しました 保存処理をしています @@ -397,7 +397,7 @@ 再生位置を削除しました ファイルが移動または削除されました ファイルを上書きできません - この名前の保留中のダウンロードがあります + 同じファイル名のダウンロードが既に進行中です ファイルの作業中に NewPipe が閉じられました デバイスに空き容量がありません ファイルが削除されたため、進行状況が失われました @@ -428,7 +428,7 @@ %s 人が聴取中 アプリを再起動すると、言語が変更されます - 高速早送り/巻き戻し時間 + 高速早送り/巻き戻し間隔 PeerTube インスタンス PeerTube インスタンスを選択する あなたに最適なインスタンスを探す: %s @@ -531,7 +531,7 @@ プレイリスト ページ プレイリストを選択してください 自動的に再生を開始します — %s - 自動キュー + 自動でキューに追加 アクティブなプレイヤーのキューが入れ替わります プレイヤーを別のプレイヤーに切り替えるとキューが置き換わる可能性があります しない @@ -539,8 +539,8 @@ キューを再生 キューを消去する前に確認する URL を認識できませんでした。他のアプリで開きますか? - 通知に表示される動画サムネイルを 16:9 から 1:1 のアスペクト比にスケールします (ゆがみが生じるかもしれません) - サムネイルを 1:1 のアスペクト比にスケールする + 通知に表示されるサムネイルを 16:9 から正方形にします + サムネイルを正方形にする 以下をタップして通知のアクションを編集します。右側にあるチェックボックスを使用してコンパクトな通知に表示するものを 3 つまで選択します コンパクトな通知に表示されるアクションは 3 つまで選ぶことができます! 5 番目のアクションボタン @@ -569,8 +569,8 @@ 動画のハッシュ化通知 動画のハッシュ化進行状況の通知 コメント - 無効にするとビデオの概要と追加情報を非表示にします - 説明を表示 + 無効にすると動画の概要欄と追加情報を非表示にします + 概要欄を表示 最近 開く 説明 @@ -612,9 +612,8 @@ サムネイルの URL ウェブサイトを開く ダウンロードのたびに保存する場所を尋ねます - ダウンロードフォルダがまだ設定されていません。今すぐデフォルトのフォルダを選択してください + ダウンロードフォルダーがまだ設定されていません。今すぐデフォルトのフォルダーを選択してください Android 10 以降は \'Storage Access Framework\' のみがサポートされます - \'Storage Access Framework\' は Android KitKat 以下ではサポートされていません 高速モードでこの情報の詳細は提供されません。 \'%s\' のフィードを読み込めませんでした。 フィードの読み込みエラー @@ -634,13 +633,13 @@ 低品質 (小) 高品質 (大) シークバーのサムネイルプレビュー - コメントは無効です + コメントは無効になっています 視聴済みとしてマーク リモート検索候補 ローカル検索候補 アイテムをスワイプして削除 直接フルスクリーンモードに切り替えて、ミニプレイヤーで動画を開始しません。自動回転がロックされている場合でも、フルスクリーンを終了することでミニプレイヤーにアクセスできます - フルスクリーンでメインプレイヤーを開始 + プレイヤーをフルスクリーンで開始 %1$s つのダウンロードを削除しました @@ -661,7 +660,7 @@ 新しいフィードアイテム エラー報告通知 エラーが発生しました。通知をご覧ください - NewPipe はエラーに遭遇しました。タップして報告 + エラーが発生しました (タップすると報告できます) スナックバーにエラーを表示 固定されたコメント この動作に適切なファイルマネージャが見つかりませんでした。 @@ -692,9 +691,18 @@ 新しいストリーム 通知 現在再生しているストリームの通知を構成 - 読み込む間隔を変更します (現在 %s)。小さい値にすると初回読み込み時間が短くなります。変更にはプレイヤーの再起動が必要です。 + 読み込み間隔を変更します (現在 %s)。小さくすると再生開始までの時間が短くなります。変更を適用するには再起動が必要です。 必要なネットワークの種類 パーセント 半音 すべてのネットワーク + データの読み込み間隔 + 未知の形式 + 未知の品質 + サポートされてない動画は表示されていません + 選択された動画は外部プレイヤーではサポートされていません + 外部プレイヤーで利用できる音声情報がありません + 外部プレイヤーで利用できる映像情報がありません + 外部プレイヤーでの品質を選択 + 次の動画を表示する \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d0d54c62d..ad37ab778 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -13,7 +13,7 @@ 다음으로 공유 비디오 다운로드 폴더 다운로드된 비디오 파일이 이 곳에 저장됩니다 - 비디오 파일이 다운로드 될 폴더를 선택하세요 + 비디오 파일 다운로드 폴더 선택 기본 해상도 Kodi로 재생 Kore를 설치할까요\? @@ -34,14 +34,14 @@ 외부 오디오 플레이어 사용 오디오 다운로드 폴더 다운로드된 오디오 파일이 이 곳에 저장됩니다 - 오디오 파일이 다운로드 될 폴더를 선택하세요 + 오디오 파일 다운로드 폴더 선택 테마 어두운 테마 밝은 테마 외관 백그라운드에서 재생 중 네트워크 오류 - \"검색\" 버튼을 눌러서 시작하세요 + \"검색\" 버튼을 눌러서 시작하세요. 컨텐츠 연령 제한 컨텐츠 보여주기 라이브 @@ -53,7 +53,7 @@ 다운로드 메뉴를 설정할 수 없습니다 죄송합니다. 오류가 발생했습니다. 이메일을 통해 이 오류 보고 - 죄송합니다. 오류가 발생했습니다. + 죄송합니다. 문제가 발생했습니다. 보고 정보: 다음이 발생함: @@ -87,15 +87,15 @@ 팝업 크기 및 위치 기억 마지막으로 사용한 팝업 위치 및 크기를 기억합니다 검색 제안 - 검색 중에 제안을 표시합니다 + 검색할 때 표시할 제안 선택 검색 기록 검색 기록을 기기에 저장합니다 기록 보기 시청했던 비디오 기록을 저장합니다 - 자동으로 다시 재생 - 전화 통화 등으로 인해 재생이 중단된 이후에 다시 재생을 시작합니다 + 재생 재개 + 중단 후 계속 재생(예: 전화 통화) \"길게 눌러 대기열에 추가하기\" 팁 표시 - 비디오 상세 정보 페이지에서 백그라운드/팝업 재생 버튼을 누를 경우 팁을 표시합니다 + 비디오 \"세부사항:\"에서 배경 또는 팝업 버튼을 누를 때 팁 표시 플레이어 동작 기록 및 캐시 @@ -114,7 +114,7 @@ 이 스트림을 재생할 수 없습니다 복구할 수 없는 플레이어 오류가 발생했습니다 플레이어 오류로부터 복구 중 - 무엇을:\\n요청:\\n컨텐츠 언어:\\컨텐츠 국가:\\n앱 언어:\\n서비스:\\nGMT 기준 시간:\\n패키지:\\n버전:\\n안드로이드 버전: + 무엇을:\\n요청:\\n컨텐츠 언어:\\n컨텐츠 국가:\\n앱 언어:\\n서비스:\\nGMT 시간:\\n패키지:\\n버전:\\nOS 버전: 결과 없음 구독할 항목을 추가하세요 @@ -157,7 +157,7 @@ 번역, 디자인, 코딩 등 다양한 기여를 언제나 환영합니다. 더 나아지도록 도와주세요! GitHub에서 보기 기부 - 여러분들의 더 나은 경험을 위해 많은 사람들이 NewPipe를 개발하는데 노력을 기울이고 있습니다. NewPipe에 참여하는 개발자들이 커피 한 잔을 즐길 수 있도록 기부해주세요. + NewPipe는 최고의 사용자 경험을 제공하기 위해 자유 시간을 보내는 자원 봉사자에 의해 개발되었습니다. 개발자가 커피 한 잔을 즐기면서 NewPipe를 더욱 개선할 수 있도록 지원해주세요. 보답하기 웹사이트 NewPipe에 관한 더 많은 정보를 얻으려면 웹사이트를 방문하세요. @@ -175,13 +175,13 @@ 키오스크 선택 인기 급상승 탑 50 - 신작 & 뜨는 동영상 + 신작 및 인기 동영상 제거 상세 정보 오디오 설정 눌러서 대기열에 추가 백그라운드에서 재생 - 새 팝업에서 재생 + 팝업에서 재생 시작 스트림 플레이어를 찾을 수 없습니다 (VLC를 설치하여 동영상을 재생할 수 있습니다). 스트림 파일 다운로드하기 정보 보기 @@ -189,8 +189,8 @@ 이곳에 추가 정확하지는 않지만 빠른 탐색 정확하지 않은 탐색은 더 빠르게 위치를 탐색할 수 있지만 정확도는 떨어집니다. 5, 15, 25초 탐색은 이 기능과 같이 작동하지 않습니다 - 다음 스트림을 자동으로 대기열에 추가하기 - 이전 스트림이 반복 재생 대기열이 아닐 경우, 관련 스트림을 자동 재생합니다 + 다음 스트림을 자동 대기열에 추가 + 이전 스트림이 반복 재생 대기열이 아닐 경우, 관련 스트림을 자동 재생 기본 콘텐츠 국가 디버그 항상 @@ -200,7 +200,7 @@ 기본으로 전환 데이터베이스 가져오기 데이터베이스 내보내기 - 현재 시청 기록 및 구독 목록을 덮어쓰기 합니다 + 현재 시청 기록, 구독, 재생 목록, (선택 사항) 설정 재정의 시청 기록과 구독 목록, 재생 목록, 설정을 내보냅니다 외부 플레이어는 이러한 종류의 링크를 지원하지 않습니다 발견된 비디오 스트림 없음 @@ -259,22 +259,21 @@ 이전 내보내기 구독 목록 가져오기 실패 구독 목록 내보내기 실패 - Google 테이크아웃을 통해 YouTube 구독 목록을 가져올 수 있습니다: + 구글 테이크아웃에서 유튜브 구독 가져오기: \n -\n1. 이곳으로 가세요: %1$s -\n2. 요청에 따라 로그인을 진행합니다 -\n3. \"모든 데이터 포함됨\"을 누른 뒤 \"모두 선택 해제\"를 누릅니다. 그 다음 \"구독정보\"를 선택하고 \"확인\"을 누릅니다 -\n4. \"다음 단계\"를 누르고 \"내보내기 생성\"을 선택합니다 -\n5. \"다운로드\" 버튼을 눌러 다운로드 한 후 -\n6. 테이크아웃 ZIP파일을 압축해제 하여 .json 파일 (일반적으로 \"YouTube 및 YouTube Music/구독/구독.json\")을 받고 가져옵니다. - SoundCloud 프로필을 가져오시려면 URL 및 ID를 입력해주세요. +\n1. 다음 URL로 이동: %1$s +\n2. 요청 시 로그인 +\n3. \"모든 데이터 포함\"을 클릭한 다음 \"모두 선택 취소\"를 클릭한 후 \"구독\"만 선택하고 \"확인\" 클릭 +\n4. \"다음 단계\"를 클릭한 다음 \"내보내기 만들기\" 클릭 +\n5. \"다운로드\" 버튼이 나타나면 클릭 +\n6. 아래 파일 가져오기를 클릭하고 다운로드한 .zip 파일 선택 +\n7. [.zip 가져오기가 실패한 경우] .csv 파일(일반적으로 \"YouTube 및 YouTube Music/subscriptions/subscriptions.csv\" 아래에 있음)의 압축을 풀고, 아래 파일 가져오기를 클릭하고 압축을 푼 csv 파일 선택 + URL 또는 ID를 입력하여 SoundCloud 프로필을 가져옵니다: \n -\n프로필 URL을 찾으시려면 다음 과정을 따라해 주세요. -\n -\n1. 웹 브라우저의 \"데스크톱 모드\" 를 활성화하세요 -\n2. 이 주소로 가세요: %1$s -\n3. 로그인이 필요하면 하세요. -\n4. 리디렉트된 프로필 URL을 복사하세요. +\n1. 웹 브라우저에서 \"데스크톱 모드\"를 활성화합니다(모바일 장치에서는 사이트를 사용할 수 없습니다) +\n2. 다음 URL로 이동: %1$s +\n3. 요청 시 로그인 +\n4. 리디렉션된 프로필 URL을 복사합니다. 프로필ID, soundcloud.com/프로필ID 경고: 데이터가 많이 소모될 수 있습니다. \n @@ -288,13 +287,13 @@ 재생 속도 조절 템포 피치 - 영상과 소리 분리 (소리가 깨질 수 있음) + 영상과 소리 분리 (왜곡이 발생할 수 있음) 다운로드 가능한 스트림이 없습니다 이 파일을 재생할 수 있는 플레이어 앱이 없습니다 선호하는 열기 동작 컨텐츠를 열 때 사용할 기본 동작 — %s 자막 - 플레이어 자막 글자 크기와 배경 스타일을 변경합니다. 이를 적용하려면 앱을 재시작 해야 합니다. + 플레이어 자막 글자 크기와 배경 스타일을 수정합니다. 적용하려면 앱을 다시 시작해야 합니다 채널 재생목록 시청 기록 삭제하기 @@ -309,10 +308,11 @@ NewPipe 프로젝트는 사용자의 개인 정보 보호를 최우선으로 생각하며, 동의 없이 어떠한 정보도 수집하지 않습니다. \nNewPipe 개인정보 보호 정책에서는 오류 보고 시 어떠한 정보가 수집되고 저장되는지 자세히 명시되어 있습니다. 개인정보 보호 정책 읽기 - NewPipe는 카피레프트 자유 소프트웨어입니다. 사용자는 이 앱을 사용, 공유, 또는 수정할 수 있고, 수정 후 재배포 시 자유 소프트웨어 재단의 GNU 라이센스 버전 3 또는 그 이상의 버전을 포함해야 합니다. + NewPipe는 카피레프트 자유 소프트웨어입니다: 마음대로 사용하고, 연구하고, 공유하고, 개선할 수 있습니다. 특히 자유 소프트웨어 재단에서 발행한 GNU 일반 공중 사용 라이센스의 조건에 따라 라이센스 버전 3 또는 (귀하의 선택에 따라) 이후 버전을 재배포 및/또는 수정할 수 있습니다. 설정도 가져오시겠습니까\? 무음 구간 빨리 감기 - 유럽 연합 일반 데이터 보호 규정 (GDPR) 에 따라, 사용자는 NewPipe 개인정보 보호 정책을 읽고 꼼꼼히 확인해야 합니다. 버그 리포트를 보내시려면 개인정보 보호 정책에 동의해주세요. + 유럽 연합 일반 데이터 보호 규정(GDPR)을 준수하기 위해 NewPipe의 개인 정보 보호 정책에 주의를 기울입니다. 주의 깊게 읽으십시오. +\n버그 보고서를 보내려면 수락해야 합니다. 동의 동의하지 않음 데이터 제한 없음 @@ -320,9 +320,9 @@ 구독 취소 탭 선택 제스처 음량 조작 - 제스처를 사용해 플레이어의 음량을 조작합니다 + 제스처를 사용하여 플레이어 볼륨 제어 제스처 밝기 조작 - 제스처를 사용해 화면 밝기를 조작합니다 + 제스처를 사용하여 플레이어 밝기 제어 업데이트 트랙 사용자 @@ -340,7 +340,7 @@ 팝업 플레이어로 최소화 단계 초기화 - 저장된 탭을 읽는 중 오류가 발생하여 기본 탭을 사용합니다 + 저장된 탭을 읽을 수 없으므로 기본 탭 사용 초기화 초기 설정으로 복원하시겠습니까\? 구독자 수를 가져올 수 없습니다 @@ -351,7 +351,7 @@ 목록 격자 자동 - NewPipe 업데이트가 있습니다! + NewPipe 업데이트를 사용할 수 있습니다! 여기를 눌러서 다운로드 완료됨 대기열에 있음 @@ -367,11 +367,11 @@ 이 이름을 가진 다운로드 된 파일이 이미 있습니다 해당 이름을 가진 다운로드가 이미 진행중입니다 오류 표시 - 지정한 폴더를 만들 수 없습니다 - 파일을 만들 수 없습니다 - 보안 연결 실패 + 대상 폴더를 만들 수 없습니다 + 파일을 생성할 수 없습니다 + 보안 연결을 설정할 수 없음 서버를 찾을 수 없습니다 - 서버에 접속할 수 없습니다 + 서버에 연결할 수 없습니다 서버가 데이터를 전송하지 않고 있습니다 서버가 다중 스레드 다운로드를 받아들이지 않습니다, @string/msg_threads = 1 를 사용해 다시 시도해보세요 HTTP 찾을 수 없습니다 @@ -406,10 +406,10 @@ 다운로드 시작 다운로드 일시정지 다운로드 위치를 묻기 - 다운로드 할때 마다 저장위치를 물을 것 입니다 - SAF 사용 - 스토리지 액세스 프레임워크(SAF)는 외장 SD카드에 다운로드 할 수 있도록 해줍니다. -\n주석: 일부 기기와 호환되지 않을 수 있습니다 + 각 다운로드를 저장할 위치를 묻는 메시지가 표시됩니다. +\n외부 SD 카드에 다운로드하려면 시스템 폴더 선택기(SAF) 활성화 + 시스템 폴더 선택기(SAF) 사용 + \'저장영역 접속 프레임워크\'를 통해 외부 SD 카드로 다운로드 가능 재생 위치 삭제 모든 재생 위치를 삭제 모든 재생 위치를 삭제하시겠습니까\? @@ -419,26 +419,26 @@ 하나의 다운로드가 동시에 진행됩니다 서비스 토글, 현재 선택된 서비스: 기본 키오스크 - 시청자가 없습니다. + 시청자가 없음 %s 시청 - 듣고 있는 사람이 없습니다. + 듣고 있는 사람 없음 %s 듣는사람 - 앱을 재시작하면 언어가 변경됩니다. + 앱이 다시 시작되면 언어가 변경됩니다 빠른-감기/되감기 찾는 시간 피어튜브 인스턴스 - 당신이 선호하는 피어튜브 인스턴스를 선택하세요. - %s에서 당신이 좋아하는 인스턴스를 찾으세요. + 선호하는 PeerTube 인스턴스 선택 + %s에서 원하는 인스턴스 찾기 인스턴스 추가하기 - 인스턴스 URL을 입력하세요. - 인스턴스를 검증할 수 없습니다. - 오직 HTTPS URL들만 지원합니다. - 인스턴스가 이미 존재합니다. + 인스턴스 URL 입력 + 인스턴스를 확인할 수 없음 + HTTPS URL만 지원 + 인스턴스가 이미 존재 로컬 - 최근에 추가됨. + 최근에 추가됨 가장 선호하는 자동생성된(업로더를 찾지못함) 복구하기 @@ -474,8 +474,8 @@ 앨범 비디오 첫번째 버튼 - 알림에 표시되는 비디오 썸네일을 16:9에서 1:1 비율로 바꿉니다. (왜곡이 생길 수도 있습니다.) - 썸네일을 1:1 비율로 하기 + 알림에 표시된 비디오 썸네일을 16:9에서 1:1 화면비로 자릅니다 + 1:1 화면비로 썸네일 자르기 %s에 대한 검색 결과 셔플 연속 재생 @@ -498,8 +498,8 @@ 시청 기록 지우기 재생목록 실행 URL을 인식할 수 없습니다. 다른 앱으로 여시겠습니까\? - 대기열을 비우기 전 확인하도록 합니다. - 안드로이드에서 썸네일의 색상에 따라 알림 색상을 조절합니다. (지원되지 않는 기기가 있을 수 있습니다.) + 대기열을 지우기 전에 확인 요청 + 안드로이드에서 썸네일 이미지의 기본 색상에 따라 알림 색상을 사용자 지정 (일부 기기에서는 사용할 수 없음) 버퍼링 다섯번째 버튼 네번째 버튼 @@ -508,8 +508,8 @@ 다른 앱 위에 표시되는 권한 부여 메타 정보 표시 색상화된 알림 - 활성화된 플레이어 대기열이 교체됩니다. - 으로(로) 열기 + 활성 플레이어 대기열 교체 + 파일 열기 시청한 것으로 처리 비활성화하면 비디오 설명과 추가 정보를 표시하지 않습니다 설명 표시 @@ -532,4 +532,177 @@ 전체화면으로 주 플레이어 시작 비디오 해시 알림 GitHub에 보고 + 다음에 대기열에 추가됨 + 다음 대기열에 넣기 + 현재 재생 중인 스트림 알림 구성 + 연령 제한(예: 18세 이상)이 있으므로 어린이에게 적합하지 않을 수 있는 콘텐츠 표시 + 오류 보고 알림 + NewPipe에 오류가 발생했습니다. 보고하려면 탭하세요 + 오류가 발생했습니다. 알림을 참조하세요 + 서식이 지정된 보고서 복사 + 관련 항목 + 댓글이 비활성화되었습니다 + + %s개의 새로운 스트림 + + 해시 계산 + 다운로드 폴더가 아직 설정되지 않았습니다. 지금 기본 다운로드 폴더를 선택하세요 + 항목을 스와이프하여 제거 + 채널 세부정보 표시 + 대기열에 넣기 + 대기열에 추가됨 + 스트림 세부정보 불러오는 중… + 미디어 터널링 비활성화 + 서비스의 원본 텍스트가 스트림 항목에 표시됩니다 + 비디오 재생 시 검은색 화면이나 끊김 현상이 발생하는 경우 미디어 터널링을 비활성화합니다 + 이미지 표시기 표시 + 원본을 나타내는 이미지 위에 피카소 컬러 리본 표시: 네트워크는 빨간색, 디스크는 파란색, 메모리는 녹색 + \"플레이어 충돌\" 표시 + 플레이어를 사용할 때 충돌 옵션을 표시합니다 + 새로운 스트림 확인 실행 + 앱 충돌 + 오류 스낵바 표시 + 새로운 스트림 알림 + 구독에서 새로운 스트림에 대해 알림 + 주파수 확인 + 필요한 네트워크 연결 + 모든 네트워크 + 업데이트 확인 + 새로운 버전을 수동으로 확인 + 자동으로 재생 시작 — %s + Wi-Fi에서만 + 절대 + 탐색막대 썸네일 미리보기 + 고품질 (크게) + 저품질 (작음) + 표시하지 않음 + 업데이트 확인 중… + 디스크에서 다운로드한 모든 파일을 지우겠습니까\? + + %1$s 다운로드 삭제됨 + + 각 다운로드를 저장할 위치를 묻는 메시지가 표시됩니다 + 장치에서 열 수 있는 앱이 없습니다 + 설명에서 텍스트 선택 비활성화 + 태그 + + + 태블릿 모드 + 지역 검색 제안 + 원격 검색 제안 + 이 동영상은 연령 제한이 있습니다. +\n연령 제한 동영상에 대한 새로운 유튜브 정책으로 인해 NewPipe는 동영상 스트림에 접속할 수 없으므로 재생할 수 없습니다. + reCAPTCHA를 해결할 때 NewPipe가 저장하는 쿠키 지우기 + 설명 + 코멘트 + 해결 + 잠금 화면 배경과 알림 모두에 썸네일 사용 + 선택한 구독이 없습니다 + 시청 항목 표시 + 자동 대기열에 추가 + + %s 다운로드 완료 + + 앱 언어 + 예, 부분적으로 본 비디오 + 카테고리 + %s에 의해 + 아래에서 좋아하는 밤 테마를 선택할 수 있습니다 + 이 영상은 유튜브 뮤직 프리미엄 회원만 볼 수 있어 뉴파이프에서 스트리밍이나 다운로드가 불가능합니다. + 이 콘텐츠는 비공개이므로 NewPipe에서 스트리밍하거나 다운로드할 수 없습니다. + 내부 + 처리 중... 시간이 걸릴 수 있습니다 + 재생 로드 간격 크기 + ExoPlayer 제약으로 인해 탐색 시간이 %d초로 설정되었습니다 + 퍼센트 + 반음 + ExoPlayer 기본 + 시스템 기본값 + 새로운 피드 항목 + 그룹화되지 않은 구독만 표시 + \'%s\'에 대한 피드를 불러올 수 없습니다. + 제작자의 계정이 해지되었습니다. +\nNewPipe는 앞으로 이 피드를 불러올 수 없습니다. +\n이 채널의 구독을 취소하겠습니까\? + 빠른 공급 모드는 이에 대한 자세한 정보를 제공하지 않습니다. + 사용 가능한 경우 전용 피드에서 가져오기 + 이 콘텐츠는 유료 사용자만 사용할 수 있으므로 NewPipe에서 스트리밍 또는 다운로드할 수 없습니다. + 지원 + 언어 + 연령 제한 + 개인 + 라이센스 + 창작자의 마음 + 개인 + 비공개 + 썸네일 URL + 호스트 + 이제 이 채널을 구독했습니다 + 알림 받기 + 알림이 비활성화되었습니다 + 웹사이트 열기 + 외부 플레이어에서 사용할 수 있는 오디오 스트림이 없음 + 선택한 스트림은 외부 플레이어가 지원하지 않습니다 + 다운로더에서 아직 지원하지 않는 스트림은 표시되지 않습니다 + 모두 토글 + , + 알 수 없는 품질 + 항목에 원래 시간 전 표시 + 피드를 불러오는 중 오류 + LeakCanary를 사용할 수 없습니다 + 메모리 누수 표시 + 피드 업데이트 임계값 + 마지막 업데이트 후 구독이 오래된 것으로 간주되기 전의 시간 — %s + 항상 업데이트 + 빠른 모드 비활성화 + 빠른 모드 활성화 + %s에 의해 제작 + 채널의 아바타 썸네일 + + 최근 + 계정이 해지됨 + %s은(는) 다음과 같은 이유를 제공: + 이것은 적어도 귀하의 국가에서 SoundCloud Go+ 트랙이므로 NewPipe에서 스트리밍하거나 다운로드할 수 없습니다. + 자동 (장치 테마) + 고정된 댓글 + 추천 + 향후 동영상 표시 + 알 수 없는 형식 + 외부 플레이어의 품질 선택 + 외부 플레이어에 사용할 수 있는 비디오 스트림이 없음 + 스트림 작성자, 스트림 콘텐츠 또는 검색 요청에 대한 추가 정보가 있는 메타 정보 상자를 숨기려면 끄세요 + 로드 간격 크기를 변경합니다 (현재 %s). 값이 낮을수록 초기 비디오 로딩 속도가 빨라질 수 있습니다. 변경하려면 플레이어를 다시 시작해야 합니다. + 충돌에 대해 논의하는 문제가 이미 존재하는지 확인하세요. 중복 티켓을 생성할 때 실제 버그를 수정하는 데 시간을 할애할 수 있습니다. + 오류 알림 생성 + 구독 선택 + 일부 서비스에서 사용할 수 있으며 일반적으로 훨씬 빠르지만 제한된 양의 항목과 종종 불완전한 정보를 반환할 수 있습니다 (예: 기간 없음, 항목 유형, 라이브 상태 없음) + 안드로이드 10부터 \'저장영역 접속 프레임워크\'만 지원됩니다 + 재생 목록에 추가되기 전과 후에 시청한 동영상은 제거됩니다. +\n확실합니까\? 이것은 취소 할 수 없습니다! + 미니 플레이어에서 동영상을 시작하지 말고 자동 회전이 잠겨 있는 경우 전체 화면 모드로 직접 전환하십시오. 전체 화면을 종료하여 미니 플레이어에 계속 접속할 수 있습니다 + 공식 + 라디오 + 설명에서 텍스트 선택 활성화 + 이 콘텐츠는 귀하의 국가에서 사용할 수 없습니다. + 좋아하는 밤 테마 선택 — %s + 다운로드가 시작되었습니다 + 이제 설명 내에서 텍스트를 선택할 수 있습니다. 선택 모드에서는 페이지가 깜박이고 링크를 클릭할 수 없는 경우가 있습니다. + 이 작업에 적합한 파일 관리자를 찾을 수 없습니다. +\n저장영역 접속 프레임워크 호환 파일 관리자를 설치하십시오. + 이 작업에 적합한 파일 관리자를 찾을 수 없습니다. +\n파일 관리자를 설치하거나 다운로드 설정에서 \'%s\'을(를) 비활성화하세요. + 피드 로딩이 너무 느리다고 생각하십니까\? 그렇다면 빠른 로딩을 활성화해 보십시오 (설정에서 변경하거나 아래 버튼을 눌러 변경할 수 있습니다). +\n +\nNewPipe는 두 가지 피드 로딩 전략을 제공합니다: +\n• 느리지만 완전한 전체 구독 채널을 가져옵니다. +\n• 빠르지만 일반적으로 완전하지는 않은 전용 서비스 엔드포인트를 사용합니다. +\n +\n둘의 차이점은 빠른 동영상은 일반적으로 항목의 길이나 유형(라이브 동영상과 일반 동영상을 구분할 수 없음)과 같은 일부 정보가 부족하고 더 적은 항목을 반환할 수 있다는 것입니다. +\n +\n유튜브는 RSS 피드로 이 빠른 방법을 제공하는 서비스의 한 예입니다. +\n +\n따라서 선택은 속도 또는 정확한 정보 중에서 선호하는 것으로 귀결됩니다. + 이 기능은 아직 NewPipe에서 지원하지 않습니다. +\n +\n이후 버전에서 지원될 예정입니다. \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index e3ffbb03a..01b2e114b 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -647,7 +647,6 @@ \nAr norite atsisakyti šio kanalo prenumeratos\? Klaida įkeliant srautą Pradedant Android 10 palaikoma tik \'Storage Access Framework\' - \'Storage Access Framework\' nėra palaikomas Android KitKat ir žemesnėse versijose Jūsų bus paklausta, kur išsaugoti kiekvieną atsiuntimą Atsiuntimo aplankas dar nenustatytas, pasirinkite numatytąjį atsiuntimų aplanką dabar Komentarai yra išjungti diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 4db63cf13..ba8d49e57 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -636,7 +636,6 @@ Izslēgt multivides tuneļošanu Izslēdziet multivides tuneļošanu, ja jums video atskaņošanas laikā parādās melns ekrāns vai aizķeršanās Rādīt krāsainas lentes virs attēliem, norādot to avotu: sarkana - tīkls, zila - disks, zaļa - atmiņa - “Krātuves Piekļuves Sistēma” ir neatbalstīta uz Android KitKat un zemākām versijām Ieslēgt teksta atlasīšanu video aprakstā Lejupielādes mape vēl nav iestatīta, izvēlieties noklusējuma lejupielādes mapi Pārvelciet objektus, lai tos noņemtu diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index edbc7785e..9e4d29cfe 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -539,7 +539,7 @@ ഫോർമാറ്റുചെയ്‌ത റിപ്പോർട്ട് പകർത്തുക പ്ലേലിസ്റ്റ് പേജ് GitHub- ൽ റിപ്പോർട്ട് ചെയ്യുക - ലഘുചിത്രം 1: 1 വീക്ഷണാനുപാതത്തിലേക്ക് സ്കെയിൽ ചെയ്യുക + ലഘുചിത്രം 1: 1 വീക്ഷണാനുപാതത്തിലേക്ക് ക്രോപ് ചെയ്യുക വീഡിയോ കവർ ചിത്രത്തിന്റെ പ്രധാന നിറത്തിന് അനുസരിച്ചു നോട്ടിഫിക്കേഷന്റെ കളർ മാറ്റാൻ ആൻഡ്രോയ്ഡിനെ അനുവദിക്കുക (ഇത് എല്ലാം ഉപകരണങ്ങളിലും ലഭ്യമല്ല ) നോട്ടിഫിക്കേഷൻ വർണ്ണാഭമാകുക ഒന്നുമില്ല @@ -547,7 +547,7 @@ ആവർത്തിക്കുക കോം‌പാക്റ്റ് അറിയിപ്പിൽ‌ കാണിക്കുന്നതിന് നിങ്ങൾക്ക് പരമാവധി മൂന്ന് പ്രവർ‌ത്തനങ്ങൾ‌ തിരഞ്ഞെടുക്കാനാകും! ഇതിനോടൊപ്പം തുറക്കുക - നോട്ടിഫിക്കേഷനിൽ കാണിക്കുന്ന വീഡിയോ കവർ ചിത്രം 16:9 എന്ന അനുപാതത്തിൽ നിന്നും 1:1 ലേക്ക് മാറ്റാം (പ്രശ്നങ്ങൾ ഉണ്ടാവാൻ സാധ്യത ) + നോട്ടിഫിക്കേഷനിൽ കാണിക്കുന്ന വീഡിയോ കവർ ചിത്രം 16:9 എന്ന അനുപാതത്തിൽ നിന്നും 1:1 ലേക്ക് ക്രോപ് ചെയ്യുക ഡൗൺലോഡ് ആരംഭിച്ചു ചുവടെ നിങ്ങളുടെ പ്രിയപ്പെട്ട രാത്രി തീം തിരഞ്ഞെടുക്കാം നിങ്ങളുടെ പ്രിയപ്പെട്ട രാത്രി തീം തിരഞ്ഞെടുക്കുക — %s @@ -639,7 +639,6 @@ ഫീഡ് ലോഡ് ചെയ്യുന്നതിൽ പിശക് സംഭവിച്ചിരിക്കുന്നു ആൻഡ്രോയ്ഡ് 10 മുതൽ മാത്രമേ \"സ്റ്റോറേജ് അക്സസ് ഫ്രെയിംവർക്ക്\" പിന്തുണക്കു എവിടെ ആണ് ഡൌൺലോഡ് ചെയ്യ്യപെടുന്ന ഓരോ ഫയൽലും സംരക്ഷിക്കപ്പെടേണ്ടത് എന്ന് തങ്കളോട് ചോദിക്കും - ആൻഡ്രോയ്ഡ് കിറ്റ് ക്യാറ്റോ അതിനു താഴെക്കോ ഉള്ളതിൽ \"സ്റ്റോറേജ് ആസസ്സ് ഫ്രെയിംവർക്ക് പിന്തുണക്കുന്നില്ല കാണിക്കരുത് കുറഞ്ഞ നിലവാരം (ചെറുത് ) ഉയർന്ന നിലവാരം (വലിയത് ) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 58b4cb08c..4bd996ede 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -618,7 +618,6 @@ Kunne ikke laste inn informasjonskanal for «%s». Kunne ikke laste inn informasjonskanal Fra Android 10 er kun «lagringstilgangsrammeverk» støttet - «Lagringstilgangsrammeverket» støttes ikke på Android KitKat og tidligere. Du vil bli spurt om hvor du vil lagre hver nedlastning Miniatyrbildeforhåndsvisning Ingen nedlastingsmappe valgt. Velg forvalgt nedlastingsmappe nå. diff --git a/app/src/main/res/values-night-v21/styles.xml b/app/src/main/res/values-night/styles.xml similarity index 73% rename from app/src/main/res/values-night-v21/styles.xml rename to app/src/main/res/values-night/styles.xml index eb39dee38..7327ac145 100644 --- a/app/src/main/res/values-night-v21/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -2,7 +2,7 @@ - diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7bdc902a5..dfcccd1ae 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -121,7 +121,7 @@ Ongeldige tekens worden vervangen door deze waarde Vervangend teken Letters en cijfers - Speciaalste tekens + Speciale tekens Abonneren Geabonneerd Abonnement opgezegd @@ -559,7 +559,7 @@ Derde actieknop Tweede actieknop Eerste actieknop - Schaal de miniatuurafbeelding van de video die getoond wordt in de notificatie van verhouding 16:9 naar 1:1 (dit kan vervorming creëren) + Schaal de miniatuurafbeelding van de video die getoond wordt in de notificatie van verhouding 16:9 naar 1:1 Schaal de miniatuurafbeelding tot verhouding 1:1 Automatisch in de wachtrij plaatsen Toon memory leaks @@ -637,7 +637,6 @@ Kan geen feed laden voor \'%s\'. Error bij het inladen van de feed Vanaf Android 10 is enkel \'Storage Access Framework\' ondersteund - Het \'Storage Access Framework\' is niet ondersteund op Android KitKat en lager U wordt gevraagd waar elk bestand wordt opgeslagen Nog geen downloadfolder gekozen, kies de standaard downloadfolder Geliefd door de maker @@ -710,4 +709,13 @@ Je bent nu geabonneerd op dit kanaal Alle gedownloade bestanden van schijf wissen\? + De geselecteerde stream wordt niet ondersteund door externe spelers + Er zijn geen videostreams beschikbaar voor externe spelers + Onbekende kwaliteit + Streams die niet ondersteund worden door de downloader, worden niet getoond + Er zijn geen geluidsstreams beschikbaar voor externe spelers + Selecteer kwaliteit voor externe spelers + Onbekend formaat + Intervalgrootte tijdens afspelen + Toon toekomstige video\'s \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 2b08ebb32..46b33206f 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -529,7 +529,6 @@ ਐਪ ਭਾਸ਼ਾ ਕੋਈ ਸਥਿਤੀ ਚੁਣੋ \'ਸਟੋਰੇਜ ਐਕਸੈੱਸ ਫ਼ਰੇਮਵਰਕ\' ਐਂਡਰਾਇਡ 10 ਤੋਂ ਕੰਮ ਕਰਨਾ ਸ਼ੁਰੂ ਕਰਦਾ ਹੈ - \'ਸਟੋਰੇਜ ਐਕਸੈੱਸ ਫ਼ਰੇਮਵਰਕ\' ਐਂਡਰਾਇਡ ਕਿਟਕੈਟ ਅਤੇ ਇਸਤੋਂ ਹੇਠਾਂ ਦੇ ਵਰਜਨਾਂ \'ਤੇ ਕੰਮ ਨਹੀਂ ਕਰਦਾ ਤੁਹਾਨੂੰ ਹਰ ਵਾਰ ਪੁੱਛਿਆ ਜਾਵੇਗਾ ਕਿ ਡਾਊਨਲੋਡ ਨੂੰ ਕਿੱਥੇ ਸਾਂਭਣਾ ਹੈ ਡਾਊਨਲੋਡ ਕੀਤੀਆਂ ਫ਼ਾਈਲਾਂ ਮਿਟਾਓ ਡਾਊਨਲੋਡ ਇਤਿਹਾਸ ਸਾਫ਼ ਕਰੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a09549fcf..a009023d5 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -575,8 +575,8 @@ Przycisk trzeciej akcji Przycisk drugiej akcji Przycisk pierwszej akcji - Skaluj miniaturę wideo wyświetlaną w powiadomieniu z proporcji 16:9 do 1:1 (może powodować zniekształcenia) - Skaluj miniaturę do proporcji 1:1 + Przycinaj miniaturę wideo wyświetlaną w powiadomieniu z proporcji 16:9 do 1:1 + Przycinaj miniaturę do proporcji 1:1 Dodano do kolejki Dodaj do kolejki Pokaż wycieki pamięci @@ -646,7 +646,6 @@ \nCzy chcesz anulować subskrypcję tego kanału\? Nie udało się załadować kanału dla „%s”. Począwszy od Androida 10 obsługiwany jest tylko systemowy selektor folderów (SAF) - Systemowy selektor folderów (SAF) nie jest obsługiwany przez system Android KitKat i niższy Zostaniesz zapytany(-na), gdzie zapisać każdy pobierany plik Nie ustawiono jeszcze folderu zapisywania, wybierz domyślny teraz Błąd podczas ładowania kanału @@ -739,4 +738,5 @@ Wybierz jakość dla zewnętrznych odtwarzaczy Nieznany format Nieznana jakość + Pokaż przyszłe wideo \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4b0af0718..9d19a8585 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -560,8 +560,8 @@ Terceiro botão de ação Segundo botão de ação Primeiro botão de ação - Dimensionar a miniatura do vídeo mostrada na notificação da proporção 16:9 para 1:1 (pode apresentar distorções) - Dimensionar a miniatura para a proporção de 1:1 + Cortar a miniatura do vídeo mostrada na notificação da proporção 16:9 para 1:1 + Cortar a miniatura para a proporção de 1:1 Mostrar vazamentos de memória Na fila Pôr na fila @@ -633,7 +633,6 @@ Não foi possível carregar o feed para \'%s\'. Erro ao carregar o feed O \'Storage Access Framework\' é compatível apenas com versões a partir do Android 10 - O \'Storage Access Framework\' não é compatível com Android KitKat e versões anteriores Você será questionado onde salvar cada download Nenhuma pasta de download definida ainda, escolha a pasta de download padrão agora Desligado @@ -718,4 +717,5 @@ Formato desconhecido Qualidade desconhecida Tamanho do intervalo de carregamento da reprodução + Mostrar vídeos futuros \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 9a9adad52..ebcda3f21 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -560,8 +560,8 @@ Terceiro botão de ação Segundo botão de ação Primeiro botão de ação - Ajustar miniatura de vídeo mostrada na notificação de 16:9 para 1:1 (pode introduzir distorções) - Ajustar miniatura à proporção de 1:1 + Cortar a miniatura de vídeo mostrada na notificação de 16:9 a 1:1 + Cortar miniatura na proporção 1:1 Mostrar \'leaks\' de memória Colocado na fila Colocar na fila @@ -638,7 +638,6 @@ Não foi possível carregar o feed para \'%s\'. Erro ao carregar o feed A partir do Android 10, apenas o \'Storage Access Framework\' é compatível - \'Storage Access Framework\' não é compatível com Android KitKat e versões anteriores Sempre que descarregar um ficheiro, terá que indicar o local para o guardar Não mostrar Baixa qualidade (menor) @@ -717,4 +716,6 @@ Formato desconhecido Qualidade desconhecida Selecione a qualidade para reprodutores externos + Tamanho do intervalo de carregamento da reprodução + Mostrar vídeos futuros \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5453b1fa4..5aa3d4f4d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,21 +1,21 @@ Publicado em %1$s - Reprodutor de vídeo não encontrado. Instalar o VLC\? + Não tem um reprodutor de vídeo. Instalar o VLC\? Instalar Cancelar Abrir no navegador - Compartilhar - Baixar + Partilhar + Descarregar Pesquisar Definições - Você quis dizer \"%1$s\"\? - Compartilhar com + Será que queria dizer \"%1$s\"\? + Partilhar com Utilizar reprodutor de vídeo externo Utilizar reprodutor de áudio externo - Pasta para os arquivos de vídeo - Os arquivos de vídeo baixados serão guardados aqui - Escolha a pasta para colocar os arquivos de vídeo + Pasta para os ficheiros de vídeo + Os ficheiros de vídeo descarregados serão guardados aqui + Escolha a pasta para colocar os ficheiros de vídeo Resolução padrão Reproduzir no Kodi Instalar Kore\? @@ -38,9 +38,9 @@ Aparência Reprodução em segundo plano Erro de rede - Pasta para arquivos de áudio - Os arquivos de áudio baixados serão guardados aqui - Escolha a pasta para colocar os arquivos de áudio + Pasta para ficheiros de áudio + Os ficheiros de áudio descarregados serão guardados aqui + Escolha a pasta para colocar os ficheiros de áudio Erro Não foi possível carregar todas as miniaturas Não foi possível decifrar a assinatura do URL @@ -60,7 +60,7 @@ Vídeo Áudio Tentar novamente - Toque nesta belíssima lupa para começar. + Toque na lupa para começar. Em direto Transferências Transferências @@ -96,7 +96,7 @@ Resolução padrão para janela popup Mostrar resoluções mais altas Apenas alguns dispositivos conseguem reproduzir vídeos em 2K/4K - Pop-up + Popup Lembrar propriedades do popup Limpar Segundo plano @@ -110,18 +110,18 @@ © %1$s de %2$s nos termos da %3$s Acerca Licenças - Aplicação livre de reprodução de emissões para Android. + Aplicação livre de reprodução de transmissões para Android. Ver no GitHub Licença do NewPipe Se tem ideias para: tradução, alterações de desenho, limpeza de código, ou alterações significativas no código fonte - todas as ajudas são bem-vindas. Quanto mais se faz, melhor ficará! Ver licença Participar - Inscrever-se - Inscrito - Canal não inscrito - Não foi possível alterar a inscrição - Não foi possível atualizar a inscrição - Inscrições + Subscrever + Subscrito + Canal não subscrito + Não foi possível alterar a subscrição + Não foi possível atualizar a subscrição + Subscrições Novidades Histórico de pesquisa Guardar termos de pesquisa localmente @@ -140,16 +140,19 @@ Sem subscritores %s subscritor + %s subscritores %s subscritores Sem visualizações %s visualização + %s visualizações %s visualizações Sem vídeos %s vídeo + %s vídeos %s vídeos Transferências @@ -197,7 +200,7 @@ Mudar nome Doar Não foi encontrado um reprodutor (pode instalar o VLC para reproduzir). - Baixar arquivo de vídeo + Descarregar ficheiro de vídeo Adicionar a Utilizar pesquisa rápida A pesquisa inexata permite que esta seja mais rápida, mas reduz a precisão. Procurar por 5, 15 ou 25 segundos não funciona corretamente @@ -206,8 +209,8 @@ Cache de imagens limpa País padrão para conteúdo Depuração - Não foram encontradas emissões de vídeo - Não foram encontradas emissões de áudio + Não foram encontrados vídeos + Não foram encontrados áudios Pasta inexistente Fonte de conteúdo/ficheiro inexistente O ficheiro não existe ou não tem permissões para ler e/ou escrever @@ -301,7 +304,7 @@ O projeto NewPipe leva a sua privacidade muito a sério. Por isso, não recolhe nenhum dado sem o seu consentimento. \nA polícia de privacidade do NewPipe explica, em detalhe, os tipos de dados enviados sempre que submete um relatório de erro. Ver política de privacidade - Enfileirar o próximo stream automaticamente + Adicionar o próximo vídeo à fila automaticamente NewPipe é um software livre \"copyleft\": pode utilizar, estudar, partilhar e melhorar a aplicação. Especificamente, pode redistribuir e/ou modificar a aplicação nos termos da GNU General Public License, conforme publicada pela Free Software Foundation, tanto a versão 3 da licença ou (por opção) qualquer versão posterior. Também deseja importar as definições\? Toque longo para colocar na fila @@ -333,11 +336,11 @@ Nenhuma Ativar reprodutor em segundo plano Ativar reprodutor \'popup\' - Desinscrever-se + Cancelar subscrição Escolher separador - Gestos para controle de volume + Gestos para controlo de volume Utilizar gestos para controlar o volume do reprodutor - Gestos para controle de brilho + Gestos para controlo de brilho Utilizar gestos para controlar o brilho do reprodutor Atualizações Ficheiro eliminado @@ -425,11 +428,13 @@ Ninguém está a ver %s a ver + %s a ver %s a ver Ninguém está a ouvir %s ouvinte + %s ouvintes %s ouvintes O idioma será alterado assim que reiniciar a app @@ -479,8 +484,9 @@ Deseja apagar este grupo\? O nome do grupo está vazio - %d selecionada - %d selecionadas + %d selecionado + %d selecionados + %d selecionados Nenhuma subscrição selecionada Selecionar subscrições @@ -491,18 +497,22 @@ Grupos de canais %d dia + %d dias %d dias %d hora + %d horas %d horas %d minuto + %d minutos %d minutos %d segundo + %d segundos %d segundos Devido às restrições de ExoPlayer, a duração da pesquisa foi definida para %d segundos @@ -526,7 +536,7 @@ Sim e também os vídeos parcialmente vistos Remover vídeos visualizados\? Remover visualizados - Os textos originais dos serviços serão visíveis nos itens de fluxo + Os textos originais dos serviços serão visíveis nos itens do vídeo Mostrar antiguidade nos itens Ativar \"Modo restrito\" do YouTube Por %s @@ -539,21 +549,21 @@ Verifique se o seu erro já foi reportado. A criação de erros em duplicado tira-nos tempo que pode ser utilizado para corrigir os erros. Reportar no GitHub Copiar relatório formatado - Mostrando resultados para: %s + A mostrar resultados para: %s Quarto botão de ação Terceiro botão de ação Segundo botão de ação Primeiro botão de ação - Ajustar miniatura de vídeo mostrada na notificação de 16:9 para 1:1 (pode haver distorções) - Ajustar miniatura à proporção de 1:1 + Ajustar miniatura de vídeo mostrada na notificação de 16:9 para 1:1 (pode introduzir distorções) + Cortar miniatura na proporção 1:1 Iniciar reprodução automaticamente — %s Reproduzir fila Nunca - Carregando + A carregar A fila do reprodutor ativo será substituída URL não reconhecido. Abrir com outra aplicação\? Enfileiramento automático - Embaralhar + Baralhar Apenas em Wi-Fi Nada Mudar de um reprodutor para outro pode substituir a sua fila @@ -590,7 +600,7 @@ Abrir com A app travou Este vídeo tem uma restrição de idade. -\nDevido às novas políticas do YouTube com vídeos com restrição de idade, o NewPipe não pode acessar nenhum dos seus fluxos de vídeo, portanto, é incapaz de reproduzi-lo. +\nDevido às novas políticas do YouTube quanto a vídeos com restrição de idade, o NewPipe não pode aceder as estes vídeos, por isso não consegue reproduzi-lo. Este conteúdo só está disponível para utilizadores que pagaram, portanto não pode ser transmitido ou descarregado pelo NewPipe. Este vídeo está disponível apenas para os membros do YouTube Music Premium, portanto não pode ser transmitido ou descarregado pelo NewPipe. Este conteúdo é privado, portanto não pode ser transmitido ou descarregado pelo NewPipe. @@ -619,7 +629,6 @@ Não foi possível carregar o feed para \'%s\'. Erro ao carregar o feed A partir do Android 10, apenas o \'Storage Access Framework\' é compatível - A \'Framework de acesso ao armazenamento\' não está disponível no Android KitKat e anteriores Pré-visualização da miniatura da barra de pesquisa Marcar como visto Desligado @@ -652,18 +661,20 @@ Sugestões de pesquisa remotas Sugestões de pesquisa locais - %1$s descarga apagada - %1$s descargas apagadas + %1$s descarga eliminada + %1$s descargas eliminadas + %1$s descargas eliminadas Descarga concluída + %s descargas concluídas %s descargas concluídas Deslizar itens para removê-los Não iniciar vídeos no reprodutor mini, mas ir diretamente ao ecrã completo se a rotação automática estiver bloqueada. Ainda pode aceder o reprodutor mini se sair do modo de ecrã completo Iniciar reprodutor principal em ecrã completo Enfileirado o próximo - Enfileirar o próximo + Pôr na fila o próximo A processar… Pode demorar um momento Procurar atualizações Verificar manualmente se existe uma nova versão @@ -679,31 +690,32 @@ Mostrar um snackbar de erro Criar uma notificação de erro Nenhum gestor de ficheiros apropriado foi encontrado para esta ação. -\nPor favor, instale um gestor de ficheiros ou tente desativar \'%s\' nas configurações de descarregar. +\nPor favor, instale um gestor de ficheiros ou tente desativar \'%s\' nas configurações de descarregar Nenhum gestor de ficheiros apropriado foi encontrado para esta ação. -\nPor favor, instale um gestor de ficheiros compatível com o Storage Access Framework. +\nPor favor, instale um gestor de ficheiros compatível com o Storage Access Framework Comentário fixado LeakCanary não está disponível Predefinido do ExoPlayer - Altere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. Se fizer alterações é necessário reiniciar. + Altere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. Se fizer alterações é necessário reiniciar Notificação do reprodutor - Configurar a notificação da reprodução do fluxo atual + Configurar a notificação da reprodução do vídeo atual Notificações - A carregar detalhes do fluxo… - Verificar se há novos fluxos - Notificações sobre novos fluxos - Notificar sobre novos fluxos de assinaturas + A carregar detalhes do vídeo… + Verificar se há novos vídeos + Notificações sobre novos vídeos + Notificar sobre novos vídeos nas assinaturas Frequência da verificação Conexão de rede necessária Qualquer rede Agora assinou este canal Alternar tudo Apagar todos os ficheiros descarregados do disco\? - Novos fluxos - Notificações sobre novos fluxos para assinaturas + Novos vídeos + Notificações sobre novos vídeos para assinaturas - %s fluxo novo - %s fluxos novos + %s vídeo novo + %s vídeos novos + %s vídeos novos Seja notificado Notificações são desativadas @@ -717,4 +729,6 @@ Formato desconhecido Qualidade desconhecida A transmissão selecionada não é suportada por reprodutores externos + Mostrar vídeos futuros + Tamanho do intervalo de carregamento da reprodução \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index fe2292ef7..52c1ef5a3 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -378,8 +378,8 @@ Al treilea buton de acțiune Al doilea buton de acțiune Primul buton de acțiune - Scalați miniatura video afișată în notificare de la raportul de aspect 16:9 la 1:1 (poate introduce distorsiuni) - Scalare miniatură la raport aspect 1:1 + Tăiați miniatura video afișată în notificare de la raportul de aspect 16:9 la 1:1 (poate introduce distorsiuni) + Tăiere miniatură la raportul de aspect 1:1 Se arată rezultate pentru:%s Nicio aplicație de pe dispozitivul dvs. nu poate deschide acesta Capitole @@ -649,7 +649,6 @@ Nu s-a putut încărca fluxul pentru \"%s\". Eroare la încărcarea fluxului Începând cu Android 10, este acceptat doar \"Storage Access Framework\" - \"Storage Access Framework\" nu este acceptat pe Android KitKat și versiunile ulterioare Veți fi întrebat unde să salvați fiecare descărcare S-a șters %1$s descărcare @@ -723,4 +722,13 @@ V-ați abonat la acest canal Comutați toate , + Fluxurile care încă nu pot fi descărcate nu sunt afișate + Fluxul selectat nu este acceptat de playerele externe + Nu sunt disponibile fluxuri audio pentru playerele externe + Nu sunt disponibile fluxuri video pentru playerele externe + Selectați calitatea pentru playerele externe + Format necunoscut + Calitate necunoscută + Dimensiunea intervalului de încărcare de redare + Afișați videoclipurile din viitor \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 64351c7d3..24f023c81 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -651,7 +651,6 @@ %s указывает следующую причину: Аккаунт отключён Начиная с Android 10 поддерживается только \"Storage Access Framework\" - \"Storage Access Framework\" не поддерживается на Android KitKat и ниже Спрашивать, куда сохранять каждую загрузку Папка для загрузки ещё не выбрана, укажите папку для загрузки сейчас Отключить @@ -664,7 +663,7 @@ Высокое качество (крупнее) Миниатюра над полосой прокрутки Автору видео понравилось это - Пометить как проигранные + Пометить проигранными Picasso: указать цветом источник изображений (красный — сеть, синий — диск, зелёный — память) Цветные метки на изображениях Серверные предложения поиска @@ -708,30 +707,32 @@ Вызвать сбой плеера Уведомление отчёта об ошибке Уведомления для отчётов об ошибках - NewPipe столкнулся с ошибкой, нажмите для отчёта + Ошибка NewPipe, нажмите для отчёта Произошла ошибка, подробнее в уведомлении - Показать ошибку снекбара - Создать уведомление об ошибке - Для этого действия не найдено подходящего файлового менеджера. -\nПожалуйста, установите файловый менеджер, или попробуйте отключить \'%s\' в настройках загрузок. - Для этого действия не найдено подходящего файлового менеджера. -\nПожалуйста, установите файловый менеджер, совместимый со Storage Access Framework (SAF). + Показать снэк-бар с ошибкой + Показать уведомление об ошибке + Не найден подходящий для этого действия файловый менеджер. +\nУстановите файловый менеджер, или попробуйте отключить \'%s\' в настройках загрузок + Не найден подходящий для этого действия файловый менеджер. +\nУстановите файловый менеджер, совместимый с Storage Access Framework Закреплённый комментарий LeakCanary недоступна Стандартное значение ExoPlayer - Изменить размер интервала загрузки (сейчас %s). Меньшее значение может ускорить начальную загрузку видео. Изменение значения потребует перезапуска плеера. - Загрузка деталей трансляции… - Проверить на наличие новых трансляций + Изменить размер предварительной загрузки (сейчас %s). Меньшее значение может ускорить загрузку видео. При изменении требуется перезапуск плеера + Загрузка сведений о трансляции… + Проверить наличие новых трансляций Удалить все загруженные файлы\? Уведомления плеера , Полутон Проценты - Выбранная трансляция не поддерживается внешними проигрывателями - Нет ни одного доступного видео потока для внешних проигрывателей + Выбранный поток не поддерживается внешними плеерами + Нет видеопотоков, доступных внешним плеерам Были скрыты трансляции, которые пока ещё не поддерживаются загрузчиком Неизвестный формат - Нет ни одного доступного аудио потока для внешних проигрывателей - Выберите качество для внешних проигрывателей + Нет аудиопотоков, доступных внешним плеерам + Выберите качество для внешних плееров Неизвестное качество + Размер предварительной загрузки + Показывать будущие видео \ No newline at end of file diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 2fc18cb6f..d3a92b116 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -550,7 +550,7 @@ Pedi una cunfirma in antis de iscantzellare una lista Òrdine casuale Modìfica cada atzione de notìfica inoghe in suta incarchende·la. Ischerta·nde finas a tres de ammustrare in sa notìfica cumpata impreende sas casellas de controllu a destra - Iscala sa miniadura ammustrada in sa notìfica dae su formadu in 16:9 a cussu 1:1 (diat pòdere causare istorchimentos) + Sega sa miniadura ammustrada in sa notìfica dae su formadu in 16:9 a cussu 1:1 Nudda Carrighende Repite @@ -560,7 +560,7 @@ Su de tres butones de atzione Su de duos butones de atzione Su de unu butone de atzione - Pone in iscala sa miniadura in formadu 1:1 + Sega sa miniadura in formadu 1:1 Ammustra sas pèrdidas de memòria Annànghidu a sa lista Pone in lista @@ -633,7 +633,6 @@ \nCheres bogare s\'iscritzione a custu canale\? Carrigamentu de su flussu pro \'%s\' fallidu. Errore carrighende su flussu - Su \'Storage Access Framework\' no est suportadu in Android KitKat e versiones prus betzas T\'at a bènnere pedidu in ue sarvare cada documentu Non b\'at galu peruna cartella de iscarrigamentu impostada. Issèbera como sa cartella de iscarrigamentu predefinida Istudadu @@ -717,4 +716,6 @@ Formadu disconnotu Calidade disconnota Su flussu seletzionadu no est galu suportadu dae letores esternos + Mannària de s\'intervallu de carrigamentu de sa riprodutzione + Ammustra sos vìdeos imbenientes \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index bfba46a5c..799ea4288 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -570,8 +570,8 @@ Akčné tlačidlo tri Akčné tlačidlo dva Akčné tlačidlo jedna - Zmeniť pomer strán videa zobrazovaného v miniatúre z 16:9 na 1:1 (čo môže spôsobovať skreslenie) - Zmenšiť pomer strán miniatúry na 1: 1 + Orezať pomer strán videa zobrazovaného v miniatúre z 16:9 na 1:1 + Orezať pomer strán miniatúry na 1: 1 Zobraziť memory leaks Zaradené do poradia Zaradiť do poradia @@ -643,7 +643,6 @@ Nemožno načítať informačný kanál \'%s\'. Chyba pri načítaní kanála \'Storage Access Framework\' je podporovaný len od Androidu 10 a vyššie - \'Storage Access Framework\' nie je podporovaný v systéme Android KitKat a ani v starších verziách Pri každom sťahovaní sa zobrazí výzva kam uložiť súbor Nie je nastavený adresár na sťahovanie, nastavte ho teraz Označiť ako videné @@ -682,7 +681,7 @@ Kontrolujú sa aktualizácie… Nové položky informačného kanála Pre túto akciu sa nenašiel vhodný správca súborov. -\nNainštalujte si správcu súborov alebo skúste vypnúť \'%s\' v nastaveniach sťahovania. +\nNainštalujte si správcu súborov alebo skúste vypnúť \'%s\' v nastaveniach sťahovania Zobraziť „zlyhať prehrávač“ LeakCanary nie je k dispozícii Pre túto akciu sa nenašiel vhodný správca súborov. @@ -697,7 +696,7 @@ Zobraziť krátke oznámenie chyby Oznámte chybu ExoPlayer preddefinovaný - Zmeniť interval načítania (aktuálne %s). Menšia hodnota môže zvýšiť rýchlosť prvotného načítania videa. Zmena vyžaduje reštart. + Zmeniť interval načítania (aktuálne %s). Menšia hodnota môže zvýšiť rýchlosť prvotného načítania videa. Zmena vyžaduje reštart Upozornenia Frekvencia kontroly Vymazať všetky stiahnuté súbory z disku\? @@ -723,4 +722,13 @@ Poltón Nahrávanie podrobností streamu… Percent + Vybraný stream nie je podporovaný externými prehrávačmi + Žiadne audio streamy nie sú k dispozícií pre externé prehrávače + Vybrať kvalitu pre externé prehrávače + Neznámy formát + Interval medzipamäte + Streamy nepodporované sťahovačom sa nezobrazujú + Žiadne video streamy nie sú k dispozícií pre externé prehrávače + Neznáma kvalita + Zobraziť budúce videá \ No newline at end of file diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index f8be2c8a5..431ef57ad 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -19,7 +19,7 @@ Batoonka hawsha sadexaad Batoonka hawsha labaad Batoonka hawsha koowaad - La ekaysii galka muuqaalka xaga ogaysiisyada ka muuqda cabirka 1:1 ayadoo laga soo baddalayo 16:9 (wuxuu keeni karaa shucaac) + La ekaysii galka muuqaalka xaga ogaysiisyada ka muuqda cabirka 1:1 ayadoo laga soo baddalayo 16:9 Galka la ekaysii cabirka 1:1 Soo bandhig istikhyaar ah in muuqaalka lagu furo xarunta madadaalada Kodi Soodhig istikhyaarka \"Ku fur Kodi\" @@ -633,7 +633,6 @@ Lama soo kicin karo bandhigga: \'%s\'. Khalad ayaa ka dhacay sookicintii Laga bilaabo Android 10 kaliya waxaa la isticmaali \'SAF\' - \'SAF\' kuma shaqeeyo Android KitKat iyo wixii ka hooseeya Dajin walba meeshii lagu kaydin lahaa ayaa lagu waydiin Iska xidh kala-leexinta muuqaalada/dhagaysiga hadaad lakulanto shaashad madow ama muuqaalka oo isistaaga Xidh kala-leexinta @@ -650,4 +649,6 @@ Tus tilmaamayaasha sawirka Soojeedinada raadinta banaanka Soojeedinada raadinta gudaha + Cabirka soodaarida udhexeeya + Jabi Daareha \ No newline at end of file diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index c5b016dd4..74d89908b 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -626,7 +626,6 @@ Nuk u arrit të ngarkohej feed-i për \'%s\'. Gabim gjatë ngarkimit të feed-it Duke nisur nga Android 10 vetëm \'Storage Access Framework\' është i mbështetur - \'Storage Access Framework\' nuk është e mbështetur në Android KitKat dhe më poshtë Ju do të pyeteni se ku doni të ruani çdo shkarkim Rrëzoje aplikacionin manualisht Ç\'aktivizo tunelin e medias nëse po hasni një ekran të zi apo ngecje gjatë luajtjes së një videoje diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 68981868c..26c3e896f 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -642,7 +642,6 @@ Не могу да учитам довод за „%s“. Грешка учитавања довода Од Андроида 10 само „Storage Access Framework“ је подржан - „Storage Access Framework“ није подржан на Андроиду 4.4 и старијим Питаће вас где да сачувате свако преузимање Фасцикла за преузимање није одређена. Изаберите подразумевану фасциклу искљ diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 53cd6909b..9a57d7b20 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -524,9 +524,9 @@ Skapad av %s Ingen spellista har bokmärkts än Visa endast prenumerationer som inte grupperats - Skala miniatyrbild till 1: 1 bildförhållande + Beskär miniatyrbild till 1: 1 bildförhållande Endast över Wi-Fi - Skala videominiatyrbilden som visas i aviseringen från 16:9- till 1:1-förhållande (kan orsaka bildförvrängning) + Beskär videominiatyrbilden som visas i aviseringen från 16:9 till 1:1 bildförhållande Starta uppspelning automatiskt — %s Uppspelningskö Kunde inte känna igen URL:en. Vill du öppna med annan app\? @@ -633,7 +633,6 @@ Visa sedda objekt Det snabba flödesläget ger inte mer information om detta. Fel vid inläsning av flödet - \"Storage Access Framework\" är inte tillgängligt på Android KitKat och tidigare versioner Markera som sedd Ej listad Aktuellt @@ -677,34 +676,34 @@ NewPipe stötte på ett fel, tryck för att rapportera Ett fel uppstod, se aviseringen Visa en fel snackbar - Skapa en fel avisering + Skapa en felavisering Ingen lämplig filhanterare hittades för denna åtgärd. -\nInstallera en filhanterare eller testa att inaktivera \'%s\' i nedladdningsinställningarna. +\nInstallera en filhanterare eller testa att inaktivera \'%s\' i nedladdningsinställningarna Ingen lämplig filhanterare hittades för denna åtgärd. -\nInstallera en filhanterare som är kompatibel med Storage Access Framework. +\nInstallera en filhanterare som är kompatibel med Storage Access Framework Fäst kommentar LeakCanary är inte tillgänglig ExoPlayer standard - Ändra inläsningsintervallets storlek (för närvarande %s). Ett lägre värde kan påskynda den första videoinläsningen. Ändringar kräver omstart av spelaren. - Validera frekvens - Kräver nätverksanslutning + Ändra inläsningsintervallets storlek (för närvarande %s). Ett lägre värde kan påskynda den första videoinläsningen. Ändringar kräver omstart av spelaren + Uppdateringsintervall + Nödvändig nätverksanslutning Alla nätverk Radera alla nedladdade filer från disken\? - Notifikationer är avstängda + Aviseringar är avstängda Bli meddelad Du har nu prenumenerat till denna kanalen - Notifikationer om nya strömmar för prenumenanter + Avisering om nya strömmar för prenumenanter %s Ny ström %s Nya strömmar Konfigurera meddelande om aktuell ström som spelas upp Kör leta efter nya strömmar - Meddela om nya strömmar från prenumeranter - Notifikationer + Meddela om nya strömmar från prenumerationer + Aviseringar Nya strömmar Laddar strömdetaljer… - Nya strömmnings notifikationer + Avisering om nya strömmar , Spelaravisering Växla alla @@ -714,4 +713,9 @@ Okänd kvalitet Inga ljudströmmar tillgängliga för externa spelare Okänt format + Videoströmmar som ännu inte stöds av nedladdaren visas inte + Inläsningsintervalls storlek + Välj kvalitet för externa spelare + Visa framtida videor + Den valda videoströmmen stöds inte av externa spelare \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 827da452a..694670be8 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -207,8 +207,8 @@ மூன்றாம் செயல் பொத்தான் இரண்டாம் செயல் பொத்தான் முதல் செயல் பொத்தான் - அறிவிப்பில் தெரியும் காணொளி சிறுபடத்தை 16: 9 முதல் 1: 1 அம்ச விகிதம் வரை அளவிடு (சிதைவுகளை அறிமுகப்படுத்தலாம்) - சிறுபடத்தை 1: 1 அம்ச விகிதத்திற்கு அளவிடு + அறிவிப்பில் தெரியும் காணொளி முகப்புபடத்தை 16: 9 முதல் 1: 1 அம்ச விகிதம் வரை அளவிட்டு பிரித்து எது + அட்டைப் படத்தை 1:1 விகிதத்தில் செதுக்கவும் %s :க்கான முடிவுகளைக் காட்டுகிறது இதனுடன் திற பற்றி @@ -359,4 +359,5 @@ சந்தாக்களைத் தேர்ந்தெடு செல்லா வரியுருக்கள் இம்மதிப்புடன் மாற்றீடுசெய்யப்படும் நயமான \'திற\' செயல் + பின்னணி சுழர்சி யின் கால அலவு \ No newline at end of file diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 3958e02b0..22ad6fbf5 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -425,4 +425,29 @@ స్వయంచాలకంగా రూపొందించబడింది (ఎక్కించినవారు కనబడుటలేదు) స్వయంచాలకంగా రూపొందించబడింది శీర్షికలు + సభ్యత్వాల కోసం కొత్త స్ట్రీమ్‌ల గురించి నోటిఫికేషన్‌లు + స్థితి పునరుద్ధరణ + లయ + కొత్త స్ట్రీమ్స్ + + %s కొత్త స్ట్రీమ్ + %s కొత్త స్ట్రీమ్స్ + + స్ట్రీమ్ వివరాలను లోడ్ చేస్తోంది… + మెమరీ లీక్‌లను చూపించు + జీవితచక్రం లేని లోపాలను నివేదించండి + ఈ చర్య నెట్‌వర్క్ ఖరీదైనదని గుర్తుంచుకోండి. +\n +\nమీరు కొనసాగించాలనుకుంటున్నారా\? + శాతం + అర్ధరాగం + ప్లేబ్యాక్ లోడ్ విరామం పరిమాణం + వస్తువులపై అసలు క్రిత సమయాన్ని చూపుము + పారవేయడం తర్వాత ఫ్రాగ్మెంట్ లేదా యాక్టివిటీ లైఫ్‌సైకిల్ వెలుపల బట్వాడా చేయలేని Rx మినహాయింపులను బలవంతంగా నివేదించడం + మెమరీ లీక్ మానిటరింగ్ హీప్ డంపింగ్ చేసినప్పుడు యాప్ స్పందించక పోవడానికి కారణం కావచ్చు + శృతి + అన్‌హుక్ (వక్రీకరణకు కారణం కావచ్చు) + అడుగు + నిశ్శబ్ద సమయంలో వేగంగా ముందుకు వెళ్లుము + ప్లేబ్యాక్ స్పీడ్ నియంత్రణలు \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 512790f7c..a20a88181 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,8 +1,8 @@ - Başlamak için büyütece dokunun. + Başlamak için büyütece dokun. %1$s tarihinde yayınlandı - Akış oynatıcısı bulunamadı. VLC yüklensin mi\? + Akış oynatıcısı bulunamadı. VLC kurulsun mu\? Yükle İptal Tarayıcıda aç @@ -13,14 +13,14 @@ Bunu mu demek istediniz: \"%1$s\"\? Şununla paylaş Dış video oynatıcı kullan - Dış ses oynatıcı kullanın + Dış ses oynatıcı kullan Video indirme klasörü İndirilen video dosyaları burada depolanır Video dosyaları için indirme klasörünü seç Ses indirme klasörü İndirilen ses dosyaları burada depolanır Ses dosyaları için indirme klasörünü seç - Varsayılan çözünürlük + Öntanımlı çözünürlük Kodi ile oynat Eksik Kore uygulaması yüklensin mi\? \"Kodi ile oynat\" seçeneğini göster @@ -103,7 +103,7 @@ Açılan pencerenin son boyutunu ve konumunu hatırla Bazı çözünürlüklerde sesi kaldırır Arama önerileri - Arama yaparken gösterilecek önerileri seçin + Ararken gösterilecek önerileri seç En iyi çözünürlük NewPipe Hakkında Üçüncü Taraf Lisansları @@ -130,7 +130,7 @@ Abonelikler Yenilikler Arama geçmişi - Arama sorgularını yerel olarak saklayın + Arama sorgularını yerel olarak sakla İzleme geçmişi İzlenen videoların kaydını tut Oynatmayı sürdür @@ -191,7 +191,7 @@ Ana Görünüme Geç Çekmeceyi Aç Çekmeceyi Kapat - Akış oynatıcı bulunamadı (Oynatmak için VLC yükleyebilirsiniz). + Akış oynatıcı bulunamadı (Oynatmak için VLC kurabilirsiniz). Her Zaman Yalnızca Bir Kez Dış oynatıcılar bu tür bağlantıları desteklemez @@ -560,8 +560,8 @@ Üçüncü eylem düğmesi İkinci eylem düğmesi Birinci eylem düğmesi - Bildirimde gösterilen video küçük resmini 16:9\'dan 1:1 en/boy oranına ölçeklendir (bozulmalara neden olabilir) - Küçük resmi 1:1 en/boy oranına ölçeklendir + Bildirimde gösterilen video küçük resmini 16:9\'dan 1:1 en boy oranına kırp + Küçük resmi 1:1 en boy oranına kırp Bellek sızıntılarını göster Sıraya eklendi Kuyruğa ekle @@ -578,7 +578,7 @@ Video dosya özetleme süreci için bildirimler Video dosya özeti bildirimi En Son - Akış oluşturucu, akış içeriği veya bir arama isteği hakkında ek bilgiler içeren meta bilgi kutularını gizlemek için kapatın + Akış oluşturucu, akış içeriği veya arama isteğiyle ilgili ek bilgiler içeren üst veri bilgi kutularını gizlemek için kapat Üst bilgiyi göster Bölümler Açıklama @@ -633,7 +633,6 @@ \'%s\' için besleme yüklenemedi. Besleme yüklenirken hata \'Depolama Erişimi Çerçevesi\' yalnızca Android 10\'dan başlayarak desteklenmektedir - \'Depolama Erişimi Çerçevesi\' Android KitKat ve altında desteklenmez Her indirmede nereye kaydedileceği sorulacak İndirme klasörü belirlenmedi, şimdi öntanımlı indirme klasörünü seçin Kapat @@ -676,15 +675,15 @@ Hata bildirimi oluştur Hata balonu göster Bu eyleme uygun dosya yönetici yok. -\nLütfen dosya yönetici kurun veya indirme ayarlarında \'%s\' devre dışı bırakın. +\nLütfen dosya yönetici kurun veya indirme ayarlarında \'%s\' devre dışı bırakın Bu eyleme uygun dosya yönetici bulunamadı. -\nLütfen Depolama Erişimi Çerçevesi uyumlu dosya yönetici kurun. +\nLütfen Depolama Erişimi Çerçevesi uyumlu dosya yönetici kurun Hata raporları için bildirimler Oynatıcı kullanırken çöktürme seçeneği gösterir Oynatıcıyı çöktür Sabitlenmiş yorum LeakCanary yok - Yükleme ara boyutunu değiştir (şu anda %s). Düşük bir değer videonun ilk yüklenişini hızlandırabilir. Değişiklikler oynatıcının yeniden başlatılmasını gerektirir. + Yükleme ara boyutunu değiştir (şu anda %s). Düşük bir değer videonun ilk yüklenişini hızlandırabilir. Değişiklikler oynatıcının yeniden başlatılmasını gerektirir ExoPlayer öntanımlısı Yeni akış bildirimleri Bildirimler @@ -694,7 +693,7 @@ Yeni akışları denetlemeyi çalıştır Oynatıcı bildirimi - Oynatılan akış bildirimini yapılandırın + Oynatılan akış bildirimini yapılandır Yeni akışlar Akış ayrıntıları yükleniyor… Abonelikler için yeni akışlarla ilgili bildirimler @@ -702,7 +701,7 @@ Bildirimler devre dışı Bildirim alın Artık bu kanala abone oldunuz - Aboneliklerden yeni akışlar hakkında bildirim gönder + Aboneliklerden yeni akışlarla ilgili bildirim gönder Denetleme sıklığı Herhangi bir ağ İndirilen tüm dosyalar diskten silinsin mi\? @@ -710,12 +709,13 @@ Tümünü değiştir Yüzde Ara ton - Seçilen yayın harici oynatıcılar tarafından desteklenmiyor - İndirici tarafından henüz desteklenmeyen yayınlar gösterilmez - Harici oynatıcılar için ses yayını yok - Harici oynatıcılar için video yayını yok - Harici oynatıcılar için kalite seçin + Seçilen akış dış oynatıcılarca desteklenmiyor + İndiricice henüz desteklenmeyen akışlar gösterilmez + Dış oynatıcılar için ses akışı yok + Dış oynatıcılar için video akışı yok + Dış oynatıcılar için nitelik seç Bilinmeyen biçim - Bilinmeyen kalite + Bilinmeyen nitelik Oynatma yükleme aralığı boyutu + Gelecekteki videoları göster \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 50e1b00b7..882b3acc8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -558,7 +558,7 @@ Кнопка третьої дії Кнопка другої дії Кнопка першої дії - Збільшити ескіз до масштабу 1:1 + Обрізати ескіз до пропорцій 1:1 Відкрити через Завантаження почалося Виберіть нічну тему — %s @@ -616,11 +616,10 @@ Кольорове сповіщення У компактному сповіщенні є не більше трьох дій! Дії можна змінити, натиснувши на них. Позначте не більше трьох для показу в компактному сповіщенні - Масштабувати мініатюру відео 16: 9 до 1:1 (можливі спотворення) + Обрізати мініатюру відео показувану в сповіщенні з пропорцій 16: 9 до 1:1 Вимкнення тунелювання медіаданих за наявності чорного екрана або гальмування під час відтворення відео Вимкнути тунелювання медіа «Фреймворк доступу до сховища» (SAF) підтримується лише починаючи з Android 10 - «Фреймворк доступу до сховища» (SAF) не підтримується в KitKat і нижче Вас питатиме, куди зберігати кожне завантаження Не вказано теки завантаження, оберіть типову теку завантаження зараз Відкрити вебсайт @@ -693,15 +692,15 @@ Сталася помилка NewPipe, торкніться, щоб звітувати Сталася помилка. Перегляньте сповіщення Для цієї дії не знайдено відповідного файлового менеджера. -\nУстановіть файловий менеджер або спробуйте вимкнути «%s» у налаштуваннях завантажень. +\nУстановіть файловий менеджер або спробуйте вимкнути «%s» у налаштуваннях завантажень Для цієї дії не знайдено відповідного файлового менеджера. -\nУстановіть файловий менеджер, сумісний зі Storage Access Framework. +\nУстановіть файловий менеджер, сумісний зі Storage Access Framework Показати панель помилок Створити сповіщення про помилку Закріплений коментар LeakCanary недоступний Типовий ExoPlayer - Змінити розмір інтервалу завантаження (наразі %s). Менше значення може прискорити початкове завантаження відео. Зміни вимагають перезапуску програвача. + Змінити розмір інтервалу завантаження (наразі %s). Менше значення може прискорити початкове завантаження відео. Зміни вимагають перезапуску програвача Ви підписалися на цей канал , Сповіщення про нові трансляції для підписок @@ -735,4 +734,5 @@ Виберіть якість для зовнішніх програвачів Невідома якість Розмір інтервалу завантаження відтворення + Показати наступні відео \ No newline at end of file diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index b58f0946d..2c3582209 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -1,6 +1,6 @@ - شروع کرنے کے لیے \"تلاش\" پر ٹیپ کریں + شروع کرنے کے لیے \"کلاں نما شیشہ\" پر ٹیپ کریں %1$s کو شائع ہوا انسٹال منسوخ کریں @@ -494,8 +494,10 @@ تیسرا ایکشن بٹن دوسرا ایکشن بٹن پہلا ایکشن بٹن - نوٹیفیکیشن میں دکھائے جانے والے ویڈیو تھمب نیل کو 16: 9 سے 1:1 پہلو تناسب (شاید بگاڑ پیدا ہوسکتا ہے) میں اسکیل کریں + نوٹیفیکیشن میں دکھائے جانے والے ویڈیو تھمب نیل کو 16: 9 سے 1:1 پہلو تناسب میں اسکیل کریں تھمب نیل کو 1:1 کی تناسب میں رکھیں %s کے لئے نتائج دکھا رہا ہے کے ساتھ کھولیں + ویڈیو پلیئر کو کریش کریں + دیکھے ہوئے کو نشان لگائیں \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml deleted file mode 100644 index 8fa00d0d8..000000000 --- a/app/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 658c96b55..0c2b7c726 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -6,7 +6,7 @@ Cài đặt Hủy Mở trong trình duyệt - Mở trong chế độ popup + Mở bằng chế độ popup Chia sẻ Tải về Tìm kiếm @@ -38,7 +38,7 @@ Chủ đề Sáng Tối - Đen + Đen (Amoled) Nhớ thuộc tính của popup Nhớ kích thước và vị trí lần trước của popup Đề xuất tìm kiếm @@ -47,7 +47,7 @@ Hiện video \"Tiếp theo\" và \"Tương tự\" URL không được hỗ trợ Hiển thị - Phát ở chế độ nền + Phát ở dưới nền Phát ở chế độ popup Nội dung Hiển thị nội dung bị giới hạn độ tuổi @@ -67,13 +67,13 @@ Nội dung không khả dụng Không thể thiết lập menu tải về Ứng dụng / Giao diện người dùng bị lỗi - Ai Da, NewPipe đã gặp lỗi. Tôi lấy làm tiếc cho bạn. + :( Lmao, app đã xảy ra lỗi. Hãy lướt xuống dưới để xem lỗi. Báo lỗi qua email Xin lỗi, đã xảy ra sự cố. Báo cáo Thông tin: Chuyện gì đã xảy ra: - Cái gì:\\nYêu cầu:\\nNgôn ngữ của nội dung:\\nVùng miền (quốc gia) của nội dung:\\nNgôn ngữ của ứng dụng:\\nDịch vụ:\\nThời gian GMT:\\nTên gói:\\nPhiên bản:\\nPhiên bản hệ điều hành: + Loại lỗi:\\nYêu cầu:\\nNgôn ngữ của nội dung:\\nVùng miền (quốc gia) của nội dung:\\nNgôn ngữ của ứng dụng:\\nDịch vụ:\\nThời gian GMT:\\nTên gói:\\nPhiên bản:\\nPhiên bản hệ điều hành: Nhận xét của bạn (bằng tiếng Anh): Chi tiết: Xem video, thời lượng: @@ -83,7 +83,7 @@ Video Âm thanh Thử lại - ngàn + nghìn triệu tỉ Bắt đầu @@ -96,7 +96,7 @@ Lỗi NewPipe đang tải xuống Chạm để biết chi tiết - Xin hãy đợi… + Đợi chút xíu nha… Đã sao chép vào clipboard Hãy chọn một thư mục để tải xuống trong phần cài đặt Chế độ popup cần quyền này @@ -108,10 +108,10 @@ © %1$s bởi %2$s dưới %3$s Thông tin Giấy phép - Trình phát nội dung nhẹ và mã nguồn mở cho Android. + Trình phát video nhẹ và mã nguồn mở cho Android. Xem trên GitHub Giấy phép của NewPipe - Sự đóng góp luôn được hoan nghênh – cho dù bạn dịch, có ý tưởng thiết kế, dọn code, hay thay đổi rất nhiều phần code. Càng làm nhiều thì ứng dụng này sẽ càng tốt! + Sự đóng góp của bạn luôn được hoan nghênh – kể cả khi bạn dịch, thay đổi giao diện, dọn code hay thay đổi những thứ khác, sự giúp đỡ của bạn vẫn đáng được trân trọng. Bạn càng làm nhiều, ứng dụng này sẽ càng tốt hơn bao giờ hết (Miễn đừng dịch vớ vẩn là được, nhé) ! Đọc giấy phép Đóng góp Ngôn ngữ nội dung ưu tiên @@ -146,7 +146,7 @@ Theo dõi các video đã xem Tiếp tục phát Tiếp tục phát sau khi bị gián đoạn (ví dụ: cuộc gọi điện thoại) - Hiển thị mẹo \"Giữ để thêm vào hàng đợi\" + Hiển thị \"Giữ để thêm vào hàng đợi\" Hiển thị mẹo khi nhấn nút phát trong nền hoặc phát trên popup trong trang \"Chi tiết\" Quốc gia nội dung mặc định Phát @@ -169,17 +169,17 @@ Chuyển sang Main Nhập cơ sở dữ liệu Xuất cơ sở dữ liệu - Ghi đè lịch sử, danh sách đăng ký, playlist (và cài đặt, nếu có chọn) hiện tại của bạn + Ghi đè lịch sử, kênh đăng ký, playlist hiện tại (và cài đặt, nếu có) của bạn Xuất lịch sử, danh sách đăng ký, playlist và cài đặt Xóa lịch sử xem - Xóa lịch sử các luồng đã phát và vị trí phát + Xóa lịch sử những video đã xem và vị trí phát Xóa toàn bộ lịch sử xem\? Lịch sử xem đã bị xóa Xóa lịch sử tìm kiếm - Xóa lịch sử của từ khóa tìm kiếm + Xóa lịch sử tìm kiếm mà bạn đã ghi Xóa toàn bộ lịch sử tìm kiếm\? Đã xóa lịch sử tìm kiếm - Không thể phát luồng này + Không thể phát video này Đã xảy ra lỗi trình phát không thể khôi phục Phục hồi lại trình phát bị lỗi Trình phát ngoài không hỗ trợ các loại liên kết này @@ -191,7 +191,7 @@ Tên tệp không được để trống Đã xảy ra lỗi: %1$s Không có luồng nào để tải về - Không có gì ở đây + Không có gì cả Kéo để sắp xếp lại Không có người đăng ký @@ -217,12 +217,12 @@ Hầu hết các ký tự đặc biệt Không có ứng dụng nào được cài đặt để phát tệp này Đóng góp - NewPipe được phát triển bởi các tình nguyện viên dành thời gian mang lại cho bạn trải nghiệm tốt nhất. Đóng góp một tách cà phê để giúp các nhà phát triển làm NewPipe tốt hơn nữa. - Trả lại + NewPipe được phát triển bởi các tình nguyện viên dành thời gian và tâm huyết của mình để mang lại cho bạn trải nghiệm tốt nhất. Đóng góp một chút xiền để giúp chúng tôi làm NewPipe tốt hơn nữa (Nếu bạn muốn). + Đôn Nét Trang web - Truy cập trang web NewPipe để biết thêm thông tin và tin tức. + Truy cập website chính thức của NewPipe để biết thêm thông tin và tin tức. Chính sách bảo mật của NewPipe - Dự án NewPipe rất coi trọng quyền riêng tư của bạn. Do đó, ứng dụng không thu thập bất kỳ dữ liệu nào mà không có sự đồng ý của bạn. + NewPipe rất coi trọng quyền riêng tư của bạn. Do đó, ứng dụng không thu thập bất kỳ dữ liệu nào mà không có sự đồng ý của bạn. \nChính sách bảo mật của NewPipe giải thích chi tiết dữ liệu nào được gửi và lưu trữ khi bạn gửi báo cáo sự cố. Đọc chính sách bảo mật NewPipe là phần mềm miễn phí copyleft: Bạn có thể sử dụng, nghiên cứu, chia sẻ và cải thiện nó theo ý của bạn. Nói cụ thể hơn, bạn có thể phân phối lại và/hoặc sửa đổi nó theo các điều khoản trong Giấy phép Công cộng GNU (GPL) được xuất bản bởi Quỹ Phần mềm Tự do (FSF), theo phiên bản 3 hoặc bất kì phiên bản nào sau này của Giấy phép (tùy ý bạn). @@ -244,8 +244,8 @@ Cảnh báo: Không thể nhập tất cả các tệp. Thao tác này sẽ ghi đè cài đặt hiện tại của bạn. Bạn cũng muốn nhập cài đặt? - Thịnh hành - Mới và hot + Thịnh hành :D + Mới và đang hot Loại bỏ Chi tiết Cài đặt âm thanh @@ -279,7 +279,7 @@ Thu phóng Tự động tạo ra Phụ đề - Sửa cỡ chữ và kiểu màu nền phụ đề. Yêu cầu khởi động lại ứng dụng để có hiệu lực + Sửa cỡ chữ, màu chữ và kiểu màu nền phụ đề. Hãy khởi động lại ứng dụng để áp dụng Theo dõi rò rỉ bộ nhớ có thể khiến ứng dụng trở nên không phản hồi khi đổ xô đống Báo các lỗi out-of-lifecycle Buộc báo cáo ngoại lệ Rx không thể gửi được bên ngoài vòng đời của mảnh hoặc hoạt động sau khi xử lý @@ -292,7 +292,7 @@ Xuất trước Không thể nhập đăng ký Không thể xuất đăng ký - Nhập danh sách đăng ký YouTube từ một bản Google Takeout: + Nhập danh sách đăng ký YouTube từ Google Takeout: \n \n1. Vào URL này: %1$s \n2. Đăng nhập khi được yêu cầu @@ -307,14 +307,14 @@ \n2. Truy cập URL này: %1$s \n3. Đăng nhập khi được hỏi \n4. Sao chép URL tiểu sử mà bạn đã được chuyển hướng đến. - Hãy nhớ rằng hoạt động này có thể khiến bạn bị trừ tiền. + Hãy nhớ rằng hoạt động này có thể khiến bạn bị mất kênh bạn đã đăng ký trước đó. \n \nBạn có muốn tiếp tục không\? Điều khiển tốc độ phát lại - Speed - Chiều cao - Bỏ gắn (có thể gây méo) - Tua đi nhanh trong khi im lặng + Tốc độ + Độ cao + Bỏ gắn (có thể gây méo nhưng vui) + Tua nhanh trong im lặng Tiếp theo Đặt lại Để tuân thủ Quy định bảo vệ dữ liệu chung của châu Âu (GDPR), chúng tôi sẽ thu hút sự chú ý của bạn đến chính sách bảo mật của NewPipe. Vui lòng đọc kỹ. @@ -322,7 +322,7 @@ Chấp nhận Từ chối Không giới hạn - Giới hạn độ phân giải khi sử dụng dữ liệu di động + Giới hạn độ phân giải khi sử dụng 3G, 4G Thu nhỏ khi chuyển qua ứng dụng khác Hành động khi chuyển sang ứng dụng khác từ trình phát chính — %s Không @@ -354,7 +354,7 @@ Danh sách Lưới Tự động - Đã có bản cập nhật NewPipe! + Đã có bản cập nhật mới! Nhấn để tải về Xong đã tạm dừng @@ -438,14 +438,14 @@ Video đã xem trước và sau khi được thêm vào playlist sẽ bị xóa. \nBạn có chắc không\? Video sẽ không thể hồi phục được! Xóa video đã xem\? - Xóa đã xem + Xóa video đã xem Mặt định hệ thống Ngôn ngữ ứng dụng \'Storage Access Framework\' cho phép tải về thẻ SD Sử dụng trình chọn thư mục của hệ thống (SAF) Xóa file đã tải về Xóa lịch sử tải về - Không thể khôi phục bản download này + Không thể khôi phục bản tải xuống này Bật tiếng Tắt tiếng Yêu thích nhất @@ -503,8 +503,8 @@ Có thể được với một số dịch vụ, thường sẽ nhanh hơn nhưng có thể bị giới hạn nội dung nhận được hoặc nội dung nhận được không đầy đủ (v.d. thời lượng, trạng thái,..) Luôn cập nhật Khoảng thời gian kể từ lần cuối cập nhật thông tin kênh trước khi nó được coi là hết hạn — %s - Ngưỡng thời gian cập nhật feed - Nguồn cấp (feed) + Ngưỡng thời gian cập nhật thông báo + Thông báo (feed) Tạo mới Bạn muốn xóa nhóm kênh này\? Tên nhóm kênh trống @@ -516,7 +516,7 @@ Đang xử lý thông báo… Số kênh không tải được: %d Đang tải thông báo… - Feed cập nhật lần cuối vào: %s + Thông báo cập nhật lần cuối vào: %s Do giới hạn của ExoPlayer, khoảng thời gian tua đã được đặt lại thành %d giây đang khôi phục Tự tạo (không tìm thấy người upload) @@ -526,11 +526,11 @@ Chỉ hiện các kênh chưa được nhóm Không bao giờ Chỉ trên Wi-Fi - Hành vi tự động phát — %s + Tự động phát — %s Phát hàng đợi (Video) Không có danh sách nào ở đây Chọn danh sách - Vui lòng kiểm tra xem vấn đề bạn đang gặp đã có báo cáo trước đó chưa. Nếu bạn tạo nhiều báo cáo trùng lặp, bạn sẽ làm tốn thời gian để chúng tôi đọc thay vì thực sự sửa lỗi. + Vui lòng kiểm tra xem vấn đề mà bạn đang gặp đã báo cáo trước đó hay chưa. Nếu bạn tạo quá nhiều báo cáo trùng lặp, bạn sẽ khiến cho chúng tôi tốn thời gian để đọc chúng thay vì sửa lỗi bạn gặp. Báo cáo trên GitHub Sao chép bản báo cáo đã được định dạng Không thể đọc URL này. Mở với app khác\? @@ -548,20 +548,20 @@ Nút hành động thứ tư Nút hành động thứ ba Nút hành động thứ hai - Nút hành đông đầu tiên - Phóng ảnh thu nhỏ của video trong thông báo từ tỉ lệ 16:9 xuống 1:1 (có thể gây méo ảnh) - Phóng ảnh thu nhỏ theo tỉ lệ 1:1 + Nút hành động đầu tiên + Chỉnh ảnh thu nhỏ của video trên thanh thông báo từ tỉ lệ 16:9 thành 1:1 (có thể gây méo ảnh) + Chỉnh ảnh thu nhỏ thành tỉ lệ 1:1 Đang hiện kết quả cho: %s - Hàng - Hiển thị nội dung không an toàn cho trẻ em vì có giới hạn độ tuổi (+18) + Thêm vào danh sách đang phát + Hiển thị nội dung không phù hợp vì có giới hạn độ tuổi (18+) Hiện ảnh thu nhỏ (thumbnail) trên nền màn hình khóa và trong thông báo Xem hình thu nhỏ - Kiểm tra bộ - Hàng - Xoá Cookie mà NewPipe lưu trữ sau khi bạn vượt + Kiểm tra bộ nhớ + Đã thêm vào danh sách đang phát + Xoá Cookie mà NewPipe lưu trữ sau khi bạn hoàn thành nó Cookie reCAPTCHA đã được xóa Xóa bỏ Cookie của reCAPCHA - YouTube cung cấp \"Chế độ hạn chế\" để ẩn nội dung người lớn + YouTube cung cấp \"Chế độ hạn chế\" để ẩn nội dung không phụ hợp Yêu cầu Android tùy chỉnh màu của thông báo theo màu chính của ảnh thu nhỏ (lưu ý rằng việc này không khả dụng trên tất cả thiết bị) Tô màu thông báo Thiết bị của bạn không có ứng dụng để mở tệp này @@ -621,21 +621,20 @@ Tài khoản đã bị chấm dứt Hiện các mục đã xem Chế độ nguồn dữ liệu nhanh không cung cấp thêm thông tin về cái này. - Tài khoản của tác giả đã bị chấm dứt. + Tài khoản của người này đã bị chấm dứt. \nNewPipe sẽ không thể tải nguồn dữ liệu này trong tương lai. \nBạn có muốn huỷ đăng ký kênh này không\? - Không thể tải nguồn dữ liệu cho \'%s\'. - Lỗi khi tải nguồn dữ liệu + Không thể tải thông báo cho \'%s\'. + Lỗi khi tải nguồn thông báo \'Storage Access Framework\' chỉ được hỗ trợ từ Android 10 trở đi - \'Storage Access Framework\' không được hỗ trợ trên Android KitKat và cũ hơn Bạn sẽ được hỏi nơi bạn muốn lưu mỗi mục tải xuống Chưa có thư mục tải xuống nào được đặt, hãy chọn thư mục tải xuống mặc định ngay Không hiện Chất lượng thấp (nhỏ hơn) Chất lượng cao (lớn hơn) Xem trước hình thu nhỏ trên thanh tua - Bình luận đang bị tắt - Đã được người tạo thả tim + Bình luận đã bị tắt + Đã được chủ kênh thả \"thính\" Đánh dấu là đã xem Hiện ruy băng được tô màu Picasso ở trên cùng các hình ảnh và chỉ ra nguồn của chúng: đỏ đối với mạng, xanh lam đối với ổ đĩa và xanh lá đối với bộ nhớ Hiện dấu chỉ hình ảnh @@ -651,11 +650,11 @@ Không bắt đầu video ở trình phát mini, mà chuyển trực tiếp thành chế độ toàn màn hình, nếu tự động xoay bị khóa. Bạn vẫn có thể truy cập trình phát mini bằng cách thoát khỏi toàn màn hình Khởi động trình phát chính ở toàn màn hình Đã cho mục tiếp vào hàng đợi - Cho mục tiếp vào hàng đợi + Cho video kế tiếp vào hàng đợi Đang thực hiện...Có thể mất một lúc Thông báo lỗi Thông báo để báo cáo lỗi - NewPipe đã gặp lỗi, nhấn để báo cáo + NewPipe đã gặp sự cố, nhấn để xem và báo cáo Có lỗi xảy ra, hãy xem thông báo Hiện \"làm trình phát dừng\" Hiện tùy chọn dừng đột ngột khi sử dụng trình phát @@ -688,7 +687,7 @@ Thông báo về video mới từ kênh bạn đã đăng ký Thời gian kiểm tra Yêu cầu kết nối mạng - Bất kỳ mạng nào (có thể tính phí) + Bất kỳ loại mạng nào (có thể tính phí) Xóa tất cả tệp đã tải xuống khỏi ổ đĩa\? Thông báo bị tắt Được thông báo @@ -697,4 +696,13 @@ Phần trăm , Nửa cung + Luồng video mà không được trình tải xuống hỗ trợ sẽ không hiện + Không có video khả dụng cho trình chạy ngoài + Video bạn chọn không hỗ trợ trình chạy bên ngoài + Video này không có âm thanh khả dụng cho trình chạy ngoài + Chọn chất lượng cho trình chạy ngoài + Định dạng không xác định (:P) + Độ phân giải không xác định + Kích thước khoảng thời gian tải + Hiện video đề xuất \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6794d6bbd..d1c28c87a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,11 +1,11 @@ - 点击搜索按钮即可开始使用 + 点击放大镜图标即可开始使用。 发布于 %1$s 在浏览器中打开 - 在悬浮窗模式下打开 - 您是不是要找:%1$s? - 找不到串流播放器 (您可以安装 VLC 进行播放)。 + 在悬浮窗中打开 + 您要找的是不是“%1$s”? + 找不到串流播放器(您可以安装 VLC 以播放串流)。 下载串流文件 安装 取消 @@ -13,17 +13,17 @@ 下载 搜索 设置 - 分享给… + 分享给 视频下载路径 - 已下载的视频存储在这里 + 已下载的视频将存储于此 请选择下载视频的保存位置 - 已下载的音频存储在这里 + 已下载的音频将存储于此 选择下载音频的储存位置 - 使用Kodi播放 + 使用 Kodi 播放 主题 浅色 - 暗黑 - 黑色 + 深色 + 暗黑 下载 不支持的 URL 外观 @@ -32,12 +32,12 @@ %s 个视频 - 禁用 + 已停用 后台播放 - 搜索建议 + 显示搜索建议 订阅 已订阅 - 观看历史 + 记录播放记录 播放器 历史记录与缓存 撤销 @@ -46,24 +46,24 @@ 仅一次 添加至 文件 - 加载缩略图 - 清除观看记录 + 加载封面 + 清空播放历史 - 最小化后台播放器 - 最小化悬浮窗播放器 + 最小化至后台播放 + 最小化至悬浮窗播放 频道 播放列表 取消订阅 更新 文件已删除 无法得知订阅人数 - 每推出新版本时,弹出应用升级通知 + 有新版本时,显示通知提示更新应用 网格 - 新版 NewPipe 已可升级! - 服务器不接受 接收 multi-threaded 下载, 以 @string/msg_threads = 1 重试 + NewPipe 可更新! + 服务器不接受多线程下载, 使用 @string/msg_threads = 1 重试 自动播放 - 清除数据 - 观看记录已删除 + 清空数据 + 播放历史已删除 喜欢 不喜欢 视频 @@ -78,156 +78,156 @@ 暂停 删除 校验 - OK + 确定 文件名 线程数 错误 点击了解详情 请稍候… - 复制至剪贴板 + 已复制到剪贴板 悬浮窗播放 - 关于NewPipe + 关于 NewPipe 第三方许可 - © %1$s :作者 %2$s (使用 %3$s ) + © %1$s 由 %2$s 遵循 %3$s 协议发布 关于 许可证 下载 文件名中允许的字符 - 无效字符将会被替换为此 + 无效字符将会被替换为该字符 字母和数字 - 最特殊字符 + 特殊字符 没有结果 - 没有订阅者 + 无人订阅 %s 位订阅者 - 没有视频 + 无视频 拖动以重新排序 - 创建 - 解除 - 重 命名 - 未安装用于播放此文件的应用程序 - 已删除1个项目。 - 哪些标签需要在主页上展示 + 新建 + 退出 + 重命名 + 未安装可播放此文件的应用程序 + 已删除一个项目。 + 自定义主页显示的标签页 列表视图模式 已完成 - 等待中… + 等待中 已暂停 - 排队中 + 已加入队列 加入队列 操作已被系统拒绝 下载失败 没有评论 切换服务,当前选择: - 找不到串流播放器。您想安装 VLC 吗? + 找不到串流播放器。是否安装 VLC? 使用外部视频播放器 使用外部音频播放器 音频下载文件夹 默认分辨率 - 找不到Kore。是否安装? - 显示“用Kodi播放”选项 - 显示“通过Kodi media center播放视频的选项” + 未找到 Kore。是否安装 Kore? + 显示“使用 Kodi 播放”选项 + 显示“通过 Kodi 媒体中心播放视频”选项 音频 默认音频格式 - 显示“下一个”和“类似的”视频 + 显示“接下来”和“相似视频” 视频和音频 - 在后台播放 + 后台播放 内容 - 受年龄限制的内容 + 显示年龄限制的内容 直播 下载 下载 - 错误报告 + 反馈错误 错误 无法加载所有缩略图 无法解密视频的 URL 签名 无法解析网址 内容不可用 无法设置下载菜单 - App UI 崩溃 - 抱歉,这不应该发生的。 - 通过电子邮件报告错误 + App/UI 崩溃 + 抱歉, 这本不该发生。 + 使用电子邮件反馈错误 抱歉,发生了一些错误。 - 报告 + 反馈 信息: 发生了什么: - 详情:\\n请求:\\n内容语言:\\n服务:\\nGMT时间:\\n包:\\n版本:\\n操作系统版本: - 您的注释(请用英文): + 详情:\\n请求:\\n内容语言:\\n内容国家:\\n客户端语言:\\n服务:\\nGMT时间:\\n包名:\\n版本:\\n操作系统版本: + 您的附加说明(请用英文): 详细信息: 播放视频,时长: 视频上传者的头像缩略图 - 字节 - NewPipe下载中 + 十亿 + NewPipe 正在下载 请稍后在设置中设定下载目录 - 用悬浮窗模式 -\n需要此权限 - reCAPTCHA验证 - 请求的新的CAPTCHA验证 + 使用悬浮窗模式 +\n需要该权限 + reCAPTCHA 验证 + 已请求新的 reCAPTCHA 验证 在悬浮窗中播放 - 默认悬浮窗分辨率 + 默认分辨率(悬浮窗模式) 使用更高的分辨率 - 仅某些设备支持播放2K / 4K视频 + 仅部分设备支持播放 2K 或 4K 视频 清除 - 记住悬浮窗的尺寸与位置 - 记住最后一次使用悬浮窗的大小和位置 - 隐藏部分没有音频的分辨率 - 显示搜索建议 + 记住悬浮窗属性(大小与位置) + 记住上一次使用悬浮窗的大小和位置 + 部分分辨率下没有音频 + 选择搜索时显示的建议 最佳分辨率 - 开源小巧的Android媒体播放器。 - 在GitHub上查看 - NewPipe开源许可证 - 你是否有想:翻译、设计、清理或重型代码更改 ——我们始终欢迎你来贡献! + 自由且小巧的 Android 媒体播放器。 + 在 GitHub 上查看 + NewPipe 的许可证 + 你是否想过要翻译、设计、清理或重构代码——我们始终欢迎你来贡献! 阅读许可证 贡献 替换字符 - 取消订阅频道 + 已取消订阅频道 无法修改订阅 无法更新订阅 订阅 最新 - 恢复前台焦点 - 中断后继续播放(例如突然来电后) - 搜索历史记录 - 在本地存储搜索查询记录 - 记录已观看视频 - 历史 - 历史 + 自动恢复播放 + 在播放被打断(例如突然来电)后恢复播放 + 记录搜索历史 + Newpipe 将在本地存储搜索历史记录 + Newpipe 将保留播放记录 + 历史记录 + 历史记录 NewPipe 通知 - NewPipe 后台播放和悬浮窗播放的通知 + NewPipe 播放器的通知 默认视频格式 行为 - 空空如也… - 0次观看 - 是否要从搜索历史记录中删除此项目? - 显示在主页面内容 + 空空如也 + 无人观看过 + 是否删除此条搜索历史记录? + 主页面的显示内容 空白页 『时下流行』页-自定义 频道页 选择一个频道 尚未订阅频道 选择一个时下流行页 - 趋势 - 前50 + 时下流行 + 前 50 最新与热门 - 显示 \"长按添加\" 说明 - 在视频详情页中,按下背景播放或悬浮窗播放按钮时显示提示 + 显示“长按加入播放队列”提示 + 在视频详情页中,长按后台播放或悬浮窗播放按钮时显示提示 无法播放此串流 - 发生无法恢复播放器错误 - 恢复播放器错误 + 发生无法处理的播放器错误 + 播放器错误 自动恢复 移除 详情 音频设置 - 长按队列 + 长按加入播放队列 [未知] 开始后台播放 - 开始在新悬浮窗中播放 + 开始在悬浮窗中播放 捐赠 - NewPipe 是由志愿者花费时间为您带来最佳体验开发的。回馈帮助开发人员在享用一杯咖啡的同时,让 NewPipe 变得更好。 + NewPipe 由志愿者开发,他们利用自己的空闲时间,为您带来最佳的用户体验。是时候回馈他们,让他们享受一杯咖啡,帮助开发者们让 NewPipe 变得更好。 回馈 - 网站 - 请访问 NewPipe 网站了解更多信息和讯息。 - 默认国家/地区 - 切换到背景播放 + 官网 + 请访问 NewPipe 网站以了解更多信息和新闻。 + 视频默认国家/地区 + 切换到后台播放 切换到悬浮窗播放 切换到主页面 打开抽屉 @@ -242,177 +242,182 @@ 正在加载请求的内容 导入数据库 导出数据库 - 覆盖当前历史记录和订阅 - 导出历史记录、订阅和播放列表 + 覆盖您的当前播放历史、订阅、播放列表和设置(可选) + 导出历史记录、订阅、播放列表和设置 导出成功 导入成功 - 没有有效的ZIP文件 + 没有有效的 ZIP 文件 警告:无法导入所有文件。 - 这将覆盖当前设置。 + 此操作会覆盖当前设置。 显示信息 - 书签 - 最后播放 - 播放最多 - 总是寻问 + 收藏 + 最近观看 + 最多观看 + 每次询问 新建播放列表 - 重 命名 + 重命名 名称 - 添加到播放列表 - 设为播放列表缩略图 + 添加至播放列表 + 设为播放列表封面 收藏播放列表 删除收藏 - 删除此播放列表? + 是否删除此播放列表? 新建播放列表成功 加入播放列表成功 - 播放列表缩略图更改成功。 + 播放列表封面已更改。 无字幕 适应屏幕 填充屏幕 - 缩放 + 缩放画面 调试 自动生成 - 『内存泄漏监视』可能导致应用在『核心转储』时无响应 - 报告『提前结束Android生命周期』错误 - 强制报告处理后的未送达的Activity或Fragment生命周期之外的Rx异常 - 使用快速不精确搜索 - 粗略定位播放:允许播放器以略低的精确度为代价换取更快的定位速度 - 自动播放下一个 - 当播放完非循环列表中的最后一个视频时,自动加入一个相关视频到播放列表 - 没有此文件夹 + 内存泄漏监测可能会导致应用在堆转储时无响应 + 报告超出生命周期的错误 + 强制报告处理后的未送达的 Activity 或 Fragment 生命周期之外的 Rx 异常 + 使用快速寻址(不精确) + 快速寻址定位允许播放器以较低精确度为代价换取更快的寻址定位速度。此功能不适用于以 5、15 或 25 秒为隔的寻址定位 + 自动将“接下来”视频加入播放队列 + 播放完(非循环)队列中的最后一个视频后,自动将一个相关视频添加到当前播放队列 + 没有该文件夹 无相似文件/内容源 - 该文件不存在 或 缺少读写该文件的权限 + 文件不存在,或缺少读写文件权限 文件名不能为空 - 发生错误: %1$s + 发生错误:%1$s 导入 - 从…导入 - 导出到… + 导入自 + 导出到 正在导入… 正在导出… 导入文件 - 以前的导出 + 先前的导出 无法导入订阅 无法导出订阅 - 通过下载导出文件来导入 YouTube 订阅: + 从 Google takeout 导入 YouTube 订阅: \n -\n1. 转到此网站: %1$s -\n2. 登录(如果需要) -\n3. 应该立即开始下载(即导出文件) - 通过键入网址或你的 ID 导入 SoundCloud 配置文件: +\n1. 打开这个网址:%1$s +\n2. 登录谷歌账号 +\n3. 单击“包含所有数据”,然后单击“取消全选”,然后仅选择“订阅”并单击“确定” +\n4. 点击“下一步”,然后点击“创建导出作业” +\n5. 出现“下载”按钮后点击它 +\n6. 单击下面的导入文件并选择下载的 .zip 文件 +\n7.(如果 .zip 导入失败)解压 .csv文件(通常在“YouTube和YouTube Music/subscriptions/subscriptions.csv”下),点击下方的导入文件,选择解压出来的 csv 文件 + 通过输入网址或你的 ID 导入 SoundCloud 配置文件: \n -\n1. 在浏览器中启用\"电脑模式\"(该网站不适用于移动设备) -\n2. 转到此 URL: %1$s -\n3. 登录(如果需要) -\n4. 复制重定向的配置文件下载地址。 +\n1. 在浏览器中启用“电脑模式“(该网站未适配移动设备); +\n2. 打开该网站:%1$s; +\n3. 登录(如果需要); +\n4. 复制得到的配置文件下载地址。 你的 ID:soundcloud.com/[你的ID] 该操作消耗大量流量, +\n \n你想继续吗? - 关闭可防止加载缩略图,节已省数据和内存使用。(若现在更改会清除内存和储存中缓存) + 关闭可禁止加载封面,节省流量和内存使用。切换该选项将立即清除内存与存储中的图片缓存 清空图像缓存成功 - 清空已缓存元数据 + 清空已缓存的元数据 清空已缓存的网页数据 清空元数据缓存成功 播放速度控制 节奏 音调 - 解除关联(可能导致失真) + 解除音视挂钩(可能导致失真) 首选“打开”操作 - 打开内容时默认操作: = %s - 无可下载的串流内容 + 打开内容时的默认操作 — %s + 没有可下载的串流 字幕 - 修改播放器字幕比例和背景样式。需要重新启动应用程序才能生效。 - 删除串流的播放历史和播放位置 - 删除全部观看记录? - 清除搜索历史记录 - 清除搜索关键词的历史记录 - 是否删除全部搜索历史记录? - 搜索历史记录已删除。 - NewPipe 是版权自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证GPLv3或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。 - 是否要同时导入设置? - NewPipe的隐私政策 + 修改播放器字幕文本比例和背景样式。重启应用后生效 + 删除串流播放历史和播放痕迹记录 + 删除全部播放历史? + 清空搜索历史 + 清空搜索历史关键词 + 是否删除全部搜索历史? + 搜索历史已删除 + NewPipe 是 Copyleft 的自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证 GPLv3 或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。 + 是否要导入设置? + NewPipe 隐私政策 NewPipe 项目非常重视您的隐私。因此,未经您的同意,应用程序不会收集任何数据。 -\nNewPipe 的隐私政策详细解释了在发送崩溃报告时发送和存储的数据。 +\nNewPipe 的隐私政策详细解释了发送崩溃报告时会发送和存储的数据。 阅读隐私政策 - 为了遵守欧盟的《通用数据保护条例》(GDPR),我们特此提醒您注意 NewPipe 的隐私政策。请您仔细阅读。 + 为了遵守欧盟的《通用数据保护条例 (GDPR)》,我们特此提醒您注意 NewPipe 的隐私政策,请您仔细阅读。 \n您必须在同意以后才能向我们发送错误报告。 接受 拒绝 无限制 - 使用移动数据时限制分辨率 + 使用移动数据播放时降低分辨率 退出应用时最小化 - 从主播放器切换到其他应用时的操作 - %s + 从主播放器切换到其他应用时的操作 — %s 静音时快进 - 滑块[比例尺] - 重 置 + 比例调整 + 重置 曲目 用户 选择标签 - 音量手势控制 + 手势控制音量 使用手势控制播放器的音量 - 亮度手势控制 + 手势控制亮度 使用手势控制播放器的亮度 视频默认语言 应用更新通知 - NewPipe有新版本的通知 + NewPipe 新版本的通知 外置存储不可用 - 无法下载到外部 SD 卡。重置下载文件夹位置? - 读取已保存标签时发生错误,因此使用者默认标签 + 无法下载到外部 SD 卡,修改下载文件夹位置? + 读取已保存标签时发生错误,因此使用默认标签 恢复默认 是否恢复默认值? 更新 列表 自动 点击下载 - 后期处理 + 处理中 生成唯一名称 覆盖 - 正在使用此名称进行下载 + 已存在一进行中并使用该名称的下载任务 显示错误 无法创建目标文件夹 无法创建文件 - 安全连接失败 + 建立安全连接失败 找不到服务器 - 无法连接到服务器 + 无法连接至服务器 服务器未发送数据 找不到 NOT FOUND 后期处理失败 停止 最大重试次数 - 取消下载前的最多尝试次数 - 在切换到移动流量网络时中断播放 - 切换至移动数据时可能有用,尽管一些下载无法被暂停 + 取消下载前的最多重试次数 + 切换到按流量计费的网络后中断下载 + 切换至移动数据时可能有用,虽然部分下载无法被暂停 事件 - 近期大会 + 会议大会 显示评论 - 禁用,以停止显示评论 + 是否隐藏评论 无法加载评论 关闭 - 恢复播放 - 恢复上次播放位置 - 列表中的位置 - 在列表中,显示视频最后一次播放时的播放位置 - 已删除播放位置记录。 - 文件被已移动或删除 - 该名称的文件已经存在 - 命名冲突,已存在具有此名称文件 + 记录播放痕迹历史 + 再次打开播放过的视频时, 自动定位到上次播放时位置 + 在列表中显示历史播放位置 + 在列表中,使用底端进度条显示某一视频上次播放时的播放位置 + 已删除播放痕迹历史 + 文件已被移动或被删除 + 同名文件已存在 + 同名的已下载文件已经存在 无法覆盖文件 - 有此名称的已暂停下载 + 已暂停下载包含此名称的任务 NewPipe 在处理文件时被关闭 设备上没有剩余储存空间 进度丢失,文件已被删除 连接超时 - 是否要清除下载历史记录或删除所有下载的文件? - 最大下载队列 - 同时只允许一个下载进行 + 是否清空下载记录或删除所有下载的文件? + 限制下载并行任务数 + 同一时间内只允许进行一个下载任务 开始下载 暂停下载 - 询问下载位置 - 系统将询问您将每次下载的保存位置 - 使用 SAF - 存储访问框架(SAF)允许下载文件到外部SD卡。 -\n注:一些设备不兼容SAF - 删除播放位置记录 - 删除所有播放位置记录 - 删除所有播放位置记录? + 总是询问下载位置 + 系统将询问您将每次下载的保存位置。 +\n如果要下载到外部 SD 卡,请启用系统文件夹选择器 (SAF) + 使用系统文件夹选择器 (SAF) + 存储访问框架(SAF)允许下载文件到外部 SD 卡 + 删除播放痕迹历史 + 删除所有播放痕迹历史 + 是否删除全部播放痕迹历史? 『时下流行』页-默认 没有人在观看 @@ -420,26 +425,284 @@ 没有人在听 - %s 人在听 + %s 位听众 - 重新启动应用后,语言将更改。 + 语言更改将在重启应用后生效 PeerTube 服务器 - 设置自己喜欢的PeerTube服务器 - 查找最适合你的服务器%s + 设置自定义 PeerTube 服务器 + 查找你需要的服务器 %s 添加服务器 - 输入服务器网址(URL) + 输入服务器网址(URL) 无法验证服务器 - 仅支持 HTTPS和URL + 仅支持 HTTPS URL 该服务器已存在 本地 最近添加 - 最喜欢的 - 自动生成的(未找到上传者) + 最受欢迎 + 自动生成的(找不到上传者) 正在恢复 无法恢复此下载 选择一个服务器 - 快进 / 快退的单位时间 - 清除下载历史记录 - 删除下载了的文件 - + 快进 / 快退的寻址定位时间间隔 + 清空下载记录 + 删除下载文件 + 授予在其他应用上层显示的权限 + 应用语言 + 系统默认 + 完成后请点击“完成” + 完成 + 视频 + + %d 秒 + + 由于 ExoPlayer 的限制,寻址间隔置为 %d 秒 + 静音 + 取消静音 + 帮助 + + %d 分钟 + + + %d 小时 + + + %d 天 + + 频道组 + 订阅最后更新:%s + 未加载:%d + 正在加载 feed… + 正在处理 feed… + 选择订阅 + 未选中任何订阅 + + 已选中 %d + + 清空组名 + 您要删除该组吗? + 新建 + Feed + Feed 更新阈值 + 上次更新后,订阅被视为过期的时间 — %s + 始终更新 + 可用时使用专用 feed 获取 + 仅在某些服务中可用,通常速度更快,但返回的视频数量可能有限,而且信息通常不完整(如无视频时长、类型,无直播状态) + 启用快速模式 + 停用快速模式 + 您是否觉得 feed 加载太慢?如果是这样,请尝试启用快速加载(可在设置中修改,也可使用下面的按钮修改) +\n +\nNewPipe 提供两种 feed 加载策略: +\n•获取整个订阅频道,很慢但是很完整。 +\n•使用专用的服务端点,比较快但通常不完整 +\n +\n两者之间的区别在于,后者通常缺少一些信息,如视频的持续时间或类型(无法区分直播视频和普通视频),并且可能返回更少的视频条目。 +\n +\nYouTube 是一个通过其 RSS feed 提供这种快速方法的服务示例。 +\n +\n因此,选择哪种方式取决于您的偏好:加载速度还是信息准确。 + NewPipe 尚不支持该内容。 +\n +\n也许未来版本会支持它。 + ∞ 部视频 + 100+ 部视频 + 艺术家 + 专辑 + 歌曲 + 该视频有年龄限制! +\n +\n如果您想要观看,请在设置中启用“%1$s”。 + 由 %s + 由 %s 创建 + 频道的头像缩略图 + 是的,包括没看完的视频 + 已经看过且在之后被加入播放列表的视频将被删除。 +\n您确定吗?操作不能被撤消! + 移除看过的视频? + 移除看过的视频 + 来自服务的原始文本将在串流项目中可见 + 在项目上显示原始时间 + 启用 YouTube“受限模式” + 仅显示未分组订阅 + 播放列表页 + 尚无收藏 + 选择播放列表 + 请先检查您的要提交的问题是否已经存在。如果你创建了重复的反馈, 就会额外耗费我们用来修复这个问题的宝贵时间。 + 在 GitHub 上反馈 + 复制已整理的报告 + 显示结果:%s + 从不 + 仅在 Wi-Fi 下 + 自动开始播放 — %s + 播放队列 + 无法识别此 URL。是否用其他应用打开\? + 自动加入播放队列 + 当前播放队列将被替换 + 从一个播放器切换到另一个播放器后,你的播放队列可能会被替换 + 清空播放队列前再次确认 + + 正在缓冲 + 随机播放 + 单曲循环 + 最多可以选择三个操作显示在紧凑通知中! + 点击编辑下面的每一个通知操作。使用右方的复选框选择在紧凑通知中显示的动作,最多可以选择三个 + 第五操作按钮 + 第四操作按钮 + 第三操作按钮 + 第二操作按钮 + 第一操作按钮 + 将通知中视频缩略图的长宽比从 16:9 裁剪到 1:1 + 裁剪缩略图至 1:1 比例 + 显示内存泄漏 + 已加入播放队列 + 加入播放队列 + 清空与本地存储的 reCAPTCHA 验证码有关的 cookie + reCAPTCHA cookie 已被清空 + 清空 reCAPTCHA cookie + YouTube提供了“受限模式”,可以隐藏潜在的成人内容 + 展示有年龄限制的、可能不适合儿童观看的内容(比如 18+) + 让 Android 系统根据视频缩略图的主色彩给通知着色(注意,该特性仅在部分设备上可用) + 自动着色通知 + 锁屏背景和通知中使用缩略图 + 显示缩略图 + 视频哈希值计算通知 + 正在计算视频哈希值时显示的通知 + 正在计算哈希值 + 最近 + 关闭以隐藏包含有关流创建者、流内容或搜索请求的附加信息的元信息框 + 显示元数据信息 + 显示简介 + 章节 + 简介 + 相关视频 + 评论 + 显示视频描述和其他信息 + 打开方式 + 设备上没有应用可以打开 + 让应用崩溃 + 此内容仅对已付费的用户可用,因此 NewPipe 无法流式传输或下载该内容。 + 该视频仅供 YouTube Music Premium 会员使用,NewPipe 无法流式传输或下载该视频。 + 此内容是私有的,因此 NewPipe 无法流式传输或下载该内容。 + 这是 SoundCloud Go +曲目,至少在你所在的国家/地区是这样,因此 NewPipe 无法流式传输或下载它。 + 此内容在你所在的国家/地区不可用。 + 这个视频有年龄限制。 +\n由于 YouTube 针对此类视频的新政策,NewPipe 无法访问其任何视频流,因此无法播放该视频。 + 处理 + 电台 + 精选 + 自动(系统主题) + 下载已开始 + 在此选择您最喜欢的夜间主题 + 选择你最喜欢的夜间主题 — %s + 夜间主题 + 显示频道详情 + 如果遇到黑屏或视频播放卡顿的情况,请停用媒体隧道 + 停用媒体隧道 + 停用简介中的文本选择功能 + 内部 + 私享 + 未分类 + 公开 + 缩略图 URL + 所在服务器 + 支持 + 语言 + 年龄限制 + 私有性 + 许可 + 标签 + 类别 + 启用简介中的文本选择功能 + 你现在可以选择简介中的文本,注意,在选择模式下,页面可能会闪烁,链接可能无法点击。 + 打开网站 + %s 提供这个原因: + 账号被终止 + 快速 Feed 模式不提供关于这个的更多信息。 + 作者账号已被终止。 +\nNewPipe 今后将无法加载此 Feed。 +\n你要退订此频道吗? + 无法加载“%s”的 Feed。 + 加载 Feed 时出错 + 仅 Android 10 及以上版本支持“存储访问框架” + 你会被问到在哪里保存每个下载 + 尚未设置下载文件夹,现在选择默认下载文件夹 + 平板模式 + 显示已观看的项目 + 关闭 + 开启 + 评论功能已停用 + 进度条缩略图预览 + 不显示 + 低品质(较小) + 高品质(较大) + 被创作者喜爱 + 标记为已观看 + 在图像顶部显示毕加索彩带,指示其来源:红色代表网络,蓝色代表磁盘,绿色代表内存 + 显示图像指示器 + 远程搜索建议 + 本地搜索建议 + + 删除了 %1$s 个下载 + + + 完成了 %s 个下载 + + 滑动即可删除项目 + 若自动旋转被锁定,不在以小窗播放器形式中播放视频,而直接切换到全屏模式。仍可以通过退出全屏以切换至小窗播放器 + 以全屏启动主播放器 + 已添加为下一个播放 + 下一个播放 + 处理中…可能需要一些时间 + 手动检查新版本 + 检查更新中… + 检查更新 + 新订阅源条目 + 显示\"使播放器崩溃\" + 在使用播放器时显示一个崩溃选项 + 使播放器崩溃 + 错误报告通知 + 提示报告错误的通知 + 发生错误,详见通知 + 显示错误警示SnackBar + 创建一条错误通知 + 找不到适合此操作的文件管理器。 +\n请安装一文件管理器或尝试在下载设置中禁用“%s” + 找不到适合此操作的文件管理器。 +\n请安装兼容存储访问框架(SAF)的文件管理器 + NewPipe 遇到了一个错误,点击此处报告此错误 + 置顶评论 + LeakCanary 不可用 + 更改加载间隔的大小(当前为 %s),较低的值可以加快视频的首次加载速度。更改需要重启播放器 + ExoPlayer 默认 + 配置当前正在播放的串流的通知 + 新串流通知 + 检查频率 + 所需的网络连接 + 通知已被禁用 + 你刚刚订阅了此频道 + + 全选 + 播放器通知 + 通知 + 新的串流 + + %s 个新音视频流 + + 被订阅的新串流的通知 + 正在加载串流详情… + 检查新串流 + 任何网络 + 清除所有下载的文件? + 获取通知 + 来自订阅的新串流的通知 + 半音 + 百分比 + 未知格式 + 没有音频流可用于外部播放器 + 选择外部播放器画质 + 外部播放器不支持所选串流 + 没有视频流可用于外部播放器 + 不显示下载器尚不支持的串流 + 未知画质 + 回放加载间隔大小 + 显示未来视频 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 399282f7c..17ed3fc29 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -85,7 +85,7 @@ 預設影片檔案格式 純黑 浮面播緊 - 所有 + 魂&我 App/界面閃退 經過:\\n請求:\\n內容語言:\\n內容國家:\\nApp 語言:\\n服務:\\nGMT 時間:\\n封裝:\\n版本:\\nOS 版本: @@ -142,12 +142,12 @@ 本機搜尋建議 遠端搜尋建議 喺本機儲存搵過嘅查詢 - 睇過嘅紀錄 + 睇過有紀錄 恢復播放 返返最後播放到去嗰個位 清單度睇到播到去邊 - 縮圖放到去 1:1 長寬比 - 顯示喺通知嘅影片縮圖由 16:9 放到去 1:1 長寬比 (話唔定會鬆郁矇) + 縮圖以 1:1 長寬比框起 + 顯示喺通知嘅影片縮圖由 16:9 剪成 1:1 長寬比 通知色彩化 等 Android 根據縮圖嘅主色自訂通知嘅顏色 (注意:唔係部部機都用得) 夜色 @@ -200,7 +200,7 @@ 撈起去浮面 匯入資料庫 主播放器用全螢幕開啟 - 如果自動旋轉鎖定,開啟影片嘅時候唔用袖珍播放器就直接飛去全螢幕模式。您仍可結束全螢幕返返去袖珍播放器 + 開啟影片嘅時候唔用袖珍播放器就直接飛去全螢幕模式,如果自動旋轉鎖定嘅話。您仍可結束全螢幕返返去袖珍播放器 認唔出呢個 URL。要唔要用另一個 app 開? YouTube 提供嘅「嚴格篩選模式」可以過濾潛在嘅成人內容 有年齡限制 (例如 18+) 故可能兒童不宜嘅內容都照顯示 @@ -308,7 +308,7 @@ 完成咗 無影片 影片播放器 - 僅限用 Wi-Fi 嘅時候 + 淨係用 Wi-Fi 嗰陣 高畫質 (大格啲) 無訂閱者 @@ -322,7 +322,7 @@ 100+ 部影片 ∞ 部影片 未開放留言 - 建立 + 加新 未設定下載資料夾,請立即揀選預設嘅下載資料夾 刪除咗 1 個項目。 執執佢 @@ -394,7 +394,7 @@ 下載 課金 匯出唔到訂閱 - 修改播放器字幕啲字嘅大細同背景款式。要重新開過個 app 先會生效 + 修改播放器字幕大細同背景款式。要重新開過個 app 先會生效 執好就撳一下「搞掂」 靜音 處理緊… 可能要等等 @@ -466,7 +466,7 @@ 喺幕後開始播放 部機冇晒位 頂櫳重試幾多次 - 若然有機會用到流動數據嘅時候,或者用得著,雖則有啲下載或者冇得暫停 + 若然有機會用到流動數據嘅時候,可能會用得著,雖則有啲下載或者冇得暫停 輪住下載 內部 私人 @@ -503,9 +503,9 @@ 顯示睇過嘅項目 %s 話理由如下: 搵唔到合適嘅檔案總管進行呢個動作。 -\n請安裝一個檔案管理程式,又或者試下喺下載設定度停用「%s」。 +\n請安裝一個檔案管理程式,又或者試下喺下載設定度停用「%s」 搵唔到合適嘅檔案總管進行呢個動作。 -\n請安裝一個與儲存空間存取框架兼容嘅檔案管理程式。 +\n請安裝一個與儲存空間存取框架兼容嘅檔案管理程式 呢部內容限區,喺您所在國家未有提供。 呢首 (至少喺您所在國家而言) 係 SoundCloud Go+ 單曲,因此 NewPipe 未能串流或下載。 呢部內容屬於私人,因此 NewPipe 未能串流或下載。 @@ -563,7 +563,6 @@ 預設開啟內容嘅時候做咩好 — %s 脫鈎 (聲音可能會失真) 飛前一格 - Android KitKat 以及樓下唔支援「儲存空間存取框架」 未有收起嘅播放清單 照單全播 未有頻道訂閱 @@ -592,8 +591,8 @@ 載入緊串流詳細資料… 通知訂閱有新加串流 檢查頻率 - 須要網絡連線 - 不拘任何網絡 + 用咩網絡連線 + 咩網絡都無所謂 收取通知 您而家訂閱咗呢個頻道 全部切換 @@ -616,7 +615,7 @@ 當喺主影片播放器轉去第個 app 嘅時候點做好 — %s 借過幕後播 借過浮面播 - 自動開啟播放 — %s + 自動開始播放 — %s 上返行人路 時間軸預覽縮圖 載入緊摘要… @@ -645,12 +644,12 @@ 著作者嘅帳戶已經被終止。 \nNewPipe 日後唔會載入到呢個摘要。 \n您要唔要取消訂閱呢個頻道? - 某啲服務會提供,通常快趣好多,但項目數量可能有限兼詳情欠奉 (例如片長、項目類型、直播狀態) + 某啲服務有提供,通常會快趣好多,但項目數量可能有限兼欠奉詳情 (例如片長、項目類型、直播狀態) 剷走播放到邊個位 係咪要全部剷走晒播放到邊個位? 百分比 半音 - 更改載入相距大細 (目前係 %s)。細啲或者會加快影片一開波嘅載入。更改要開過個播放器至生效。 + 更改載入斬件大細 (目前係 %s)。細啲或者可以等條片快啲開波。更改要開過個播放器至生效 問過先至將排隊播清零 目前播放器入面嘅排隊播將會清零 加一個站 @@ -663,4 +662,47 @@ 揀選一個站 轉換播放器嘅時候,排隊播可能會清零 NewPipe 係「著佐權」(copyleft) 自由軟件:您可以隨意使用、考究、分享同改進佢。具體而言,您可以依據自由軟件基金會發佈嘅《GNU 通用公眾特許條款》第 3 版或 (按您選擇) 之後任一版本之下嘅條款,重新分發及/或修改呢個軟件。 + 載入斬件大細 + 互動頁面 + 預設嘅互動站 + 輸入 URL 或者您嘅 ID 去匯入 SoundCloud 個人檔案: +\n +\n一、喺網頁瀏覽器啟用「桌面版模式」(個網唔支援手機版) +\n二、去呢個網址:%1$s +\n三、叫您就登入 +\n四、複製佢彈返您去個人檔案嗰版個 URL。 + 您個 ID、soundcloud.com/您個id + 揀選互動站 + 顯示返項目原底話時隔幾耐 + 停用多媒體隧道 + 頻道成軍 + 成軍名留空 + 黃袍 + 您係咪要刪除呢個成軍? + 淨係顯示未成軍嘅訂閱 + 啲圖都要騷 Picasso 三色碼顯示源頭:紅碼係網絡上高落嚟,藍碼係儲存喺磁碟本地,綠碼係潛伏喺記憶體中 + 服務原底嘅字會騷返喺串流項目上面 + 影像要推三色碼 + 顯示定預告上畫嘅影片 + 若果播片嘅時候窒下窒下或者黑畫面,就停用多媒體隧道啦 + 點樣用 Google 匯出嚟匯入 YouTube 訂閱: +\n +\n一、去呢個網址:%1$s +\n二、叫您就登入 +\n三、撳一下「包含所有資料」,再撳一下「全部不選」,之後淨係剔返「訂閱」,然後撳「確定」 +\n四、撳一下「下一步」然後揀「建立匯出」 +\n五、個掣騷出嚟嘅時候就撳一下「下載」 +\n六、返返嚟呢度,喺下低撳「匯入檔案」,揀返下載咗嗰個 .zip 檔案 +\n七、[個 .zip 匯入唔到點算好] 將個 .csv 檔案解壓縮抽返出嚟 (通常係擺喺「YouTube and YouTube Music/subscriptions/subscriptions.csv」),喺下低撳「匯入檔案」,揀返抽出嚟個 csv 檔案 + 係咪覺得摘要「懸浮於半路太久,可否再快兩步」?可以試下啟用快速載入 (您可以喺設定度更改,又或者撳一下下低個掣)。 +\n +\nNewPipe 提供兩種載入摘要嘅方針: +\n• 攞晒成個訂閱頻道,慢得嚟志在夠完整。 +\n• 用特設嘅服務終端,快得嚟啲料爭少少。 +\n +\n兩者嘅分別在於,快趣嗰個通常都係爭噉啲料:譬如話項目嘅片長同類型 (分唔到係直播定上載),同埋攞返嚟數目可能會少啲。 +\n +\nYouTube 就係其中一個服務,有用 RSS 摘要提供呢個快趣嘅門路。 +\n +\n所以就睇您點揀:想快定要準。 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index db6caf56d..ba3cd2f98 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -550,8 +550,8 @@ 第三動作按鈕 第二動作按鈕 第一動作按鈕 - 將通知中顯示的影片縮圖從 16:9 縮放到 1:1(可能會導致變形) - 把縮圖縮放到 1:1 的長寬比 + 將通知中顯示的影片縮圖從 16:9 裁剪到 1:1 + 將縮圖裁剪為 1:1 的長寬比 顯示記憶體洩漏 已加入佇列 加入佇列 @@ -623,7 +623,6 @@ 無法載入「%s」。 載入 feed 時發生錯誤 從 Android 10 開始僅支援「儲存空間存取框架」 - Android KitKat 或更舊的版本不支援「儲存空間存取框架」 每次下載都會詢問您要下載到哪裡 尚未設定下載資料夾,立刻選擇預設的下載資料夾 關閉 @@ -665,15 +664,15 @@ 顯示錯誤 SnackBar 建立錯誤通知 找不到適合此動作的檔案管理程式。 -\n請安裝相容於儲存空間存取框架的檔案管理員。 +\n請安裝相容於儲存空間存取框架的檔案管理員 找不到適合此動作的檔案管理程式。 -\n請安裝檔案管理程式或在下載設定嘗試停用「%s」。 +\n請安裝檔案管理程式或在下載設定嘗試停用「%s」 NewPipe 遇到錯誤,點擊以回報 發生錯誤,請檢視通知 釘選的留言 LeakCanary 無法使用 ExoPlayer 預設值 - 變更載入間隔大小(目前為 %s)。較低的值可能會提昇初始影片載入速度。變更需要重新啟動播放器。 + 變更載入間隔大小(目前為 %s)。較低的值可能會提昇初始影片載入速度。變更需要重新啟動播放器 播放器通知 通知 正在載入串流詳細資訊…… @@ -705,4 +704,5 @@ 沒有可用於外部播放程式的視訊串流 選取外部播放程式的畫質 播放載入間隔大小 + 顯示未來影片 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 281fd9ce7..73e8a0cb1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,6 +6,9 @@ #CD201F + #999999 + #6C6C6C + #EEEEEE #EEEEEE diff --git a/app/src/main/res/values/colors_services.xml b/app/src/main/res/values/colors_services.xml index d6cd73d52..f3487810a 100644 --- a/app/src/main/res/values/colors_services.xml +++ b/app/src/main/res/values/colors_services.xml @@ -2,53 +2,37 @@ #e53935 - #992722 #000000 - #e53935 #992722 - #7a1717 #FFFFFF - #992722 #f57c00 - #995700 #000000 - #f57c00 #a35300 - #7d4000 #FFFFFF - #a35300 #ff6f00 - #c43e00 #000000 - #ff6f00 #a34700 - #942f00 #FFFFFF - #a34700 #9e9e9e #000000 - #9e9e9e #878787 #FFFFFF - #878787 #17a0c4 #000000 - #17a0c4 #1383a1 #FFFFFF - #1383a1 \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index c13caf610..732ff108a 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -284,6 +284,7 @@ feed_update_threshold_key 300 feed_show_played_items + feed_show_future_items show_thumbnail_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a1c220f0..e685ca081 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,8 +50,8 @@ Show \"Play with Kodi\" option Display an option to play a video via Kodi media center Crash the player - Scale thumbnail to 1:1 aspect ratio - Scale the video thumbnail shown in the notification from 16:9 to 1:1 aspect ratio (may introduce distortions) + Crop thumbnail to 1:1 aspect ratio + Crop the video thumbnail shown in the notification from 16:9 to 1:1 aspect ratio First action button Second action button Third action button @@ -79,7 +79,7 @@ Inexact seek allows the player to seek to positions faster with reduced precision. Seeking for 5, 15 or 25 seconds doesn\'t work with this Fast-forward/-rewind seek duration Playback load interval size - Change the load interval size (currently %s). A lower value may speed up initial video loading. Changes require a player restart. + Change the load interval size (currently %s). A lower value may speed up initial video loading. Changes require a player restart Ask for confirmation before clearing a queue Switching from one player to another may replace your queue The active player queue will be replaced @@ -615,7 +615,6 @@ You will be asked where to save each download Use system folder picker (SAF) The \'Storage Access Framework\' allows downloads to an external SD card - The \'Storage Access Framework\' is not supported on Android KitKat and below Starting from Android 10 only \'Storage Access Framework\' is supported Choose an instance App language @@ -685,6 +684,7 @@ \n \nSo the choice boils down to what you prefer: speed or precise information. Show watched items + Hide watched items This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version. Channel\'s avatar thumbnail Created by %s @@ -695,8 +695,10 @@ Recent Chapters No app on your device can open this - No appropriate file manager was found for this action.\nPlease install a file manager or try to disable \'%s\' in the download settings. - No appropriate file manager was found for this action.\nPlease install a Storage Access Framework compatible file manager. + No appropriate file manager was found for this action. +\nPlease install a file manager or try to disable \'%s\' in the download settings + No appropriate file manager was found for this action. +\nPlease install a Storage Access Framework compatible file manager This content is not available in your country. This is a SoundCloud Go+ track, at least in your country, so it cannot be streamed or downloaded by NewPipe. This content is private, so it cannot be streamed or downloaded by NewPipe. @@ -747,4 +749,6 @@ Select quality for external players Unknown format Unknown quality + Show future items + Hide future items \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e711b35ab..164f10672 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,27 +1,29 @@ - + + - - - - diff --git a/app/src/main/res/values/styles_services.xml b/app/src/main/res/values/styles_services.xml index ce67f468b..db1fba397 100644 --- a/app/src/main/res/values/styles_services.xml +++ b/app/src/main/res/values/styles_services.xml @@ -1,16 +1,22 @@ - - - diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index fa15af406..48e7bbd2e 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -12,6 +12,7 @@ app:iconSpaceReserved="false" /> AUDIO_STREAMS_TEST_LIST = Arrays.asList( + private static final List AUDIO_STREAMS_TEST_LIST = List.of( generateAudioStream("m4a-128-1", MediaFormat.M4A, 128), generateAudioStream("webma-192", MediaFormat.WEBMA, 192), generateAudioStream("mp3-64", MediaFormat.MP3, 64), @@ -30,7 +29,7 @@ public class ListHelperTest { generateAudioStream("mp3-192", MediaFormat.MP3, 192), generateAudioStream("webma-320", MediaFormat.WEBMA, 320)); - private static final List VIDEO_STREAMS_TEST_LIST = Arrays.asList( + private static final List VIDEO_STREAMS_TEST_LIST = List.of( generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false), generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false), generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false), @@ -38,7 +37,7 @@ public class ListHelperTest { generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false), generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false)); - private static final List VIDEO_ONLY_STREAMS_TEST_LIST = Arrays.asList( + private static final List VIDEO_ONLY_STREAMS_TEST_LIST = List.of( generateVideoStream("mpeg_4-720-1", MediaFormat.MPEG_4, "720p", true), generateVideoStream("mpeg_4-720-2", MediaFormat.MPEG_4, "720p", true), generateVideoStream("mpeg_4-2160", MediaFormat.MPEG_4, "2160p", true), @@ -54,7 +53,7 @@ public class ListHelperTest { List result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true, VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, true, false); - List expected = Arrays.asList("144p", "240p", "360p", "480p", "720p", "720p60", + List expected = List.of("144p", "240p", "360p", "480p", "720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); assertEquals(expected.size(), result.size()); @@ -69,7 +68,7 @@ public class ListHelperTest { result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true, VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, false, false); - expected = Arrays.asList("2160p60", "2160p", "1440p60", "1080p60", "1080p", "720p60", + expected = List.of("2160p60", "2160p", "1440p60", "1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p"); assertEquals(expected.size(), result.size()); for (int i = 0; i < result.size(); i++) { @@ -83,7 +82,7 @@ public class ListHelperTest { null, VIDEO_ONLY_STREAMS_TEST_LIST, true, true); List expected = - Arrays.asList("720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); + List.of("720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); assertEquals(expected.size(), result.size()); for (int i = 0; i < result.size(); i++) { @@ -97,7 +96,7 @@ public class ListHelperTest { result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true, VIDEO_STREAMS_TEST_LIST, null, false, true); - expected = Arrays.asList("720p", "480p", "360p", "240p", "144p"); + expected = List.of("720p", "480p", "360p", "240p", "144p"); assertEquals(expected.size(), result.size()); for (int i = 0; i < result.size(); i++) { assertEquals(expected.get(i), result.get(i).getResolution()); @@ -110,10 +109,10 @@ public class ListHelperTest { result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, true, VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, true, true); - expected = Arrays.asList("144p", "240p", "360p", "480p", "720p", "720p60", + expected = List.of("144p", "240p", "360p", "480p", "720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); final List expectedVideoOnly = - Arrays.asList("720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); + List.of("720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); assertEquals(expected.size(), result.size()); for (int i = 0; i < result.size(); i++) { @@ -131,7 +130,7 @@ public class ListHelperTest { final List result = ListHelper.getSortedStreamVideosList(MediaFormat.MPEG_4, false, VIDEO_STREAMS_TEST_LIST, VIDEO_ONLY_STREAMS_TEST_LIST, false, false); - final List expected = Arrays.asList( + final List expected = List.of( "1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p"); assertEquals(expected.size(), result.size()); for (int i = 0; i < result.size(); i++) { @@ -141,7 +140,7 @@ public class ListHelperTest { @Test public void getDefaultResolutionTest() { - final List testList = Arrays.asList( + final List testList = new ArrayList<>(List.of( generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false), generateVideoStream("v3gpp-240", MediaFormat.v3GPP, "240p", false), generateVideoStream("webm-480", MediaFormat.WEBM, "480p", false), @@ -149,7 +148,7 @@ public class ListHelperTest { generateVideoStream("mpeg_4-240", MediaFormat.MPEG_4, "240p", false), generateVideoStream("webm-144", MediaFormat.WEBM, "144p", false), generateVideoStream("mpeg_4-360", MediaFormat.MPEG_4, "360p", false), - generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false)); + generateVideoStream("webm-360", MediaFormat.WEBM, "360p", false))); VideoStream result = testList.get(ListHelper.getDefaultResolutionIndex( "720p", BEST_RESOLUTION_KEY, MediaFormat.MPEG_4, testList)); assertEquals("720p", result.getResolution()); @@ -223,7 +222,7 @@ public class ListHelperTest { // Doesn't contain the preferred format // //////////////////////////////////////// - List testList = Arrays.asList( + List testList = List.of( generateAudioStream("m4a-128", MediaFormat.M4A, 128), generateAudioStream("webma-192", MediaFormat.WEBMA, 192)); // List doesn't contains this format @@ -237,7 +236,7 @@ public class ListHelperTest { // Multiple not-preferred-formats and equal bitrates // ////////////////////////////////////////////////////// - testList = new ArrayList<>(Arrays.asList( + testList = new ArrayList<>(List.of( generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192), generateAudioStream("m4a-192-1", MediaFormat.M4A, 192), generateAudioStream("webma-192-2", MediaFormat.WEBMA, 192), @@ -290,7 +289,7 @@ public class ListHelperTest { // Doesn't contain the preferred format // //////////////////////////////////////// - List testList = new ArrayList<>(Arrays.asList( + List testList = new ArrayList<>(List.of( generateAudioStream("m4a-128", MediaFormat.M4A, 128), generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192))); // List doesn't contains this format @@ -310,7 +309,7 @@ public class ListHelperTest { // Multiple not-preferred-formats and equal bitrates // ////////////////////////////////////////////////////// - testList = new ArrayList<>(Arrays.asList( + testList = new ArrayList<>(List.of( generateAudioStream("webma-192-1", MediaFormat.WEBMA, 192), generateAudioStream("m4a-192-1", MediaFormat.M4A, 192), generateAudioStream("webma-256", MediaFormat.WEBMA, 256), @@ -337,7 +336,7 @@ public class ListHelperTest { @Test public void getVideoDefaultStreamIndexCombinations() { - final List testList = Arrays.asList( + final List testList = List.of( generateVideoStream("mpeg_4-1080", MediaFormat.MPEG_4, "1080p", false), generateVideoStream("mpeg_4-720_60", MediaFormat.MPEG_4, "720p60", false), generateVideoStream("mpeg_4-720", MediaFormat.MPEG_4, "720p", false), diff --git a/assets/buddy_channel_item.svg b/assets/buddy_channel_item.svg deleted file mode 100644 index 4dec41f9d..000000000 --- a/assets/buddy_channel_item.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/assets/dummy_thumbnail.svg b/assets/dummy_thumbnail.svg new file mode 100644 index 000000000..bdea80b55 --- /dev/null +++ b/assets/dummy_thumbnail.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/dummy_thumbnail_playlist.svg b/assets/dummy_thumbnail_playlist.svg new file mode 100644 index 000000000..bd4b190aa --- /dev/null +++ b/assets/dummy_thumbnail_playlist.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index bea444fab..739f2e618 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -20,6 +20,6 @@ allprojects { google() mavenCentral() maven { url "https://jitpack.io" } - maven { url "https://clojars.org/repo" } + maven { url "https://repo.clojars.org" } } } diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index 282358f6a..ce378a65f 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -116,6 +116,10 @@ + + + + diff --git a/fastlane/metadata/android/az/changelogs/988.txt b/fastlane/metadata/android/az/changelogs/988.txt new file mode 100644 index 000000000..525c0f8cc --- /dev/null +++ b/fastlane/metadata/android/az/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] Videonu başlatmağa başladıqda "Heç bir yayım əldə etmək mümkün olmadı" xətası aradan qaldırıldı +[YouTube] Vide sorğusu əvəzinə "Bu məzmun bu tətbiqdə əlçatan deyil" xətası aradan qaldırıldı diff --git a/fastlane/metadata/android/bn/changelogs/66.txt b/fastlane/metadata/android/bn/changelogs/66.txt new file mode 100644 index 000000000..05aeec04c --- /dev/null +++ b/fastlane/metadata/android/bn/changelogs/66.txt @@ -0,0 +1,33 @@ +# v0.13.7 এর পরিবর্তনসূচী + +### স্থির +- v0.13.6 এর বাছাই করা ফিল্টার সমস্যাগুলি ঠিক করুন + +# v0.13.6 এর পরিবর্তনসূচী + +### উন্নতি + +- বার্গারমেনু আইকন অ্যানিমেশন #1486 অক্ষম করা হয়েছে +- ডাউনলোড #1472 মুছে ফেলা হয়েছে পূর্বাবস্থায় +- শেয়ার মেনু #1498 এ ডাউনলোড এর অপশন +- লং ট্যাপ মেনু #1454 এ শেয়ার অপশন যোগ করা হয়েছে +- প্রস্থান #1354 এ প্রধান প্লেয়ার ছোট করা যাবে +- লাইব্রেরি সংস্করণ আপডেট এবং ডাটাবেস ব্যাকআপ নিষ্কাশন #1510 +- ExoPlayer 2.8.2 আপডেট #1392 + - দ্রুত গতি পরিবর্তনের জন্য বিভিন্ন ধাপের আকার সমর্থন করতে প্লেব্যাক গতি নিয়ন্ত্রণ ডায়ালগ পুনরায় কাজ করেছে৷ + - প্লেব্যাক গতি নিয়ন্ত্রণে নীরবতার সময় ফাস্ট-ফরওয়ার্ডে একটি টগল যুক্ত করা হয়েছে। এটি অডিওবুক এবং নির্দিষ্ট সঙ্গীত ঘরানার জন্য সহায়ক হওয়া উচিত এবং একটি সত্যিকারের বিরামহীন অভিজ্ঞতা আনতে পারে (এবং প্রচুর নীরবতার সাথে একটি গান ভাঙতে পারে =\\)। + - ম্যানুয়ালি না করে প্লেয়ারে অভ্যন্তরীণভাবে মিডিয়ার পাশাপাশি মেটাডেটা পাস করার অনুমতি দেওয়ার জন্য রিফ্যাক্টর করা মিডিয়া সোর্স রেজোলিউশন। এখন আমাদের কাছে মেটাডেটার একটি একক উৎস আছে এবং প্লেব্যাক শুরু হলে সরাসরি উপলব্ধ। + - প্লেলিস্ট ফ্র্যাগমেন্ট খোলার সময় নতুন মেটাডেটা উপলব্ধ হলে রিমোট প্লেলিস্ট মেটাডেটা আপডেট হচ্ছে না। + - বিভিন্ন UI ফিক্স: #1383, ব্যাকগ্রাউন্ড প্লেয়ার নোটিফিকেশন কন্ট্রোল এখন সবসময় সাদা, ফ্লিংিংয়ের মাধ্যমে পপআপ প্লেয়ার বন্ধ করা সহজ +- মাল্টি সার্ভিসের জন্য রিফ্যাক্টরযুক্ত আর্কিটেকচার সহ নতুন এক্সট্র্যাক্টর ব্যবহার করুন + +### সমাধান + +- #1440 ব্রোকেন ভিডিও ইনফো লেআউট #1491 ঠিক করুন +- দেখুন ইতিহাস সংশোধন #1497 + - #1495, ব্যবহারকারী প্লেলিস্ট অ্যাক্সেস করার সাথে সাথে মেটাডেটা (থাম্বনেল, শিরোনাম এবং ভিডিও গণনা) আপডেট করে। + - #1475, ডাটাবেসে একটি ভিউ রেজিস্টার করে যখন ব্যবহারকারী ডিটেইল ফ্র্যাগমেন্টে এক্সটার্নাল প্লেয়ারে একটি ভিডিও শুরু করে। +- পপআপ মোডের ক্ষেত্রে ক্রিনের সময়সীমা ঠিক করুন। #1463 (স্থির #640) +- প্রধান ভিডিও প্লেয়ার ফিক্স #1509 + - [#1412] ফিক্সড রিপিট মোড প্লেয়ার এনপিই সৃষ্টি করে যখন প্লেয়ার অ্যাক্টিভিটি ব্যাকগ্রাউন্ডে থাকে তখন নতুন ইন্টেন্ট পাওয়া যায়। + - পপআপের জন্য ফিক্সড মিনিমাইজিং প্লেয়ার প্লেয়ারকে ধ্বংস করে না যখন পপআপের অনুমতি না দেওয়া হয়। diff --git a/fastlane/metadata/android/bn/changelogs/730.txt b/fastlane/metadata/android/bn/changelogs/730.txt index fd2b8e1bb..4d0e49a84 100644 --- a/fastlane/metadata/android/bn/changelogs/730.txt +++ b/fastlane/metadata/android/bn/changelogs/730.txt @@ -1,2 +1,2 @@ -# ঠিক করা -- দ্রুত ঠিককরণ ডিক্রিপ্ট ফাংশন ত্রুটি আবারো। +# নিস্কাসিত +- দ্রুত হট নিষ্কাসন ডিক্রিপ্ট ফাংশন ত্রুটি আরেকবার। diff --git a/fastlane/metadata/android/bn/changelogs/770.txt b/fastlane/metadata/android/bn/changelogs/770.txt index 0c853ea74..5653c91ff 100644 --- a/fastlane/metadata/android/bn/changelogs/770.txt +++ b/fastlane/metadata/android/bn/changelogs/770.txt @@ -1,4 +1,4 @@ ০.১৭.২ এ পরিবর্তনসূচী -ঠিককরণ +নিষ্কাসন • কোনো ভিডিও নেই সমস্যা ঠিক করা হয়েছে diff --git a/fastlane/metadata/android/bn/changelogs/956.txt b/fastlane/metadata/android/bn/changelogs/956.txt index a01a4c8ed..48caa0b16 100644 --- a/fastlane/metadata/android/bn/changelogs/956.txt +++ b/fastlane/metadata/android/bn/changelogs/956.txt @@ -1 +1 @@ -[ইউটিউব] যখন কোন ভিডিও লোড হচ্ছিলো ক্র্যাশ ফিক্স করা হয়েছে +[ইউটিউব] কোনো ভিডিও লোড হওয়ার মধ্যের ক্র্যাশ নিষ্কাসন করা হয়েছে diff --git a/fastlane/metadata/android/bn/short_description.txt b/fastlane/metadata/android/bn/short_description.txt index 1085f6c73..6c5a1ea73 100644 --- a/fastlane/metadata/android/bn/short_description.txt +++ b/fastlane/metadata/android/bn/short_description.txt @@ -1 +1 @@ -অ্যানড্রয়েডের জন্য একটি মুক্ত ও হালকা ইউটিউব ফ্রন্টএন্ড। +অ্যানড্রয়েডের জন্য একটি মুক্ত ও সরল ইউটিউব ফ্রন্টএন্ড। diff --git a/fastlane/metadata/android/bn_BD/changelogs/63.txt b/fastlane/metadata/android/bn_BD/changelogs/63.txt new file mode 100644 index 000000000..efb6558d3 --- /dev/null +++ b/fastlane/metadata/android/bn_BD/changelogs/63.txt @@ -0,0 +1,8 @@ +### উন্নতিসমূহ +- Import/export সেটিংস #1333 +- চিত্রণ কমানো (দক্ষতার উন্নতি) #1371 +- ছোটখাটো কোডে উন্নতি #1375 +- GDPR এর সবকিছু যোগ হওয়া #1420 + +### নিষ্কাসীত +- ডাউনলোডার: .giga ফাইল থেকে অসমাপ্ত ডাউনলোড গুলোর থেমে যাওয়া লোড এর নিষ্কাসন #1407 diff --git a/fastlane/metadata/android/bn_BD/changelogs/64.txt b/fastlane/metadata/android/bn_BD/changelogs/64.txt new file mode 100644 index 000000000..502ceb579 --- /dev/null +++ b/fastlane/metadata/android/bn_BD/changelogs/64.txt @@ -0,0 +1,8 @@ +### উন্নতিসমুহ +- মোবাইল ডেটা ব্যাবহারে ভিডিও মানে সীমা দেয়ার ক্ষমতা যোগ করা হয়েছে। #1339 +- সেশন এর মাধ্যমে উজ্জ্বলতা মনে রাখা। #1442 +- দুর্বল সিপিইউগুলোতে ডাউনলোডের দক্ষতার উন্নতি। #1431 +- মিডিয়া সেশনগুলোই সাহায্যকারী যোগ করা হয়েছে। #1433 + +### নিষ্কাশন +- ডাউনলোডগুলো খুলতে বিধ্বস্ত হওয়া নিষ্কাসন( ছেড়ে রাখা নির্মাণ গুলোর জন্যেও নিষ্কাসন উপলুদ্ধ) #1441 diff --git a/fastlane/metadata/android/bn_BD/full_description.txt b/fastlane/metadata/android/bn_BD/full_description.txt new file mode 100644 index 000000000..ff00b080f --- /dev/null +++ b/fastlane/metadata/android/bn_BD/full_description.txt @@ -0,0 +1 @@ +নিউপাইপ গুগলের বা ইউটিউবের কোনো ফ্রেমওয়ার্ক লাইব্রেরি ব্যাবহার করেনা। এটা শুধু ওয়েবসাইট গুলোকে পারস করে যে তথ্যগুলো দরকার সেগুলোর প্রয়োজনে। এজন্যেই এই অ্যাপটা গুগলের কোনো সেবা ইনস্টল করা ছাড়াই ব্যাবহার করা যায়। আর, নিউপাইপ ব্যাবহার করতে তোমার কোনো ইউটিউব একাউন্ট প্রয়োজন হবে না, আর এইটা ফেশোর মতো। diff --git a/fastlane/metadata/android/bn_BD/short_description.txt b/fastlane/metadata/android/bn_BD/short_description.txt new file mode 100644 index 000000000..c14b3261b --- /dev/null +++ b/fastlane/metadata/android/bn_BD/short_description.txt @@ -0,0 +1 @@ +অ্যান্ড্রয়েড এর জন্যে একটা মুক্ত সরল ইউটিউব ফ্রন্টএন্ড। diff --git a/fastlane/metadata/android/cs/changelogs/63.txt b/fastlane/metadata/android/cs/changelogs/63.txt new file mode 100644 index 000000000..f98de65de --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/63.txt @@ -0,0 +1,8 @@ +### Improvements +- Import/export settings #1333 +- Reduce overdraw (performance improvement) #1371 +- Small code improvements #1375 +- Add everything about GDPR #1420 + +### Fixed +- Downloader: Fix crash on loading unfinished downloads from .giga files #1107 diff --git a/fastlane/metadata/android/cs/changelogs/64.txt b/fastlane/metadata/android/cs/changelogs/64.txt new file mode 100644 index 000000000..7b2bd1ce5 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/64.txt @@ -0,0 +1,8 @@ +### Vylepšení +- Přidáni možnosti omezení kvality videa při použití mobilních dat. #1339 +- Zapamatování jasu pro relaci. #1442 +- Zlepšený výkon pro stahování se slabším CPU #1431 +- Přidána (fungující) podpora pro mediální relace #1433 + +### Oprava +- Opraveno selhání aplikace při otevření stáhnutých souborů (oprava je nyní k dispozici pro vydané sestavy. #1441 diff --git a/fastlane/metadata/android/cs/changelogs/65.txt b/fastlane/metadata/android/cs/changelogs/65.txt new file mode 100644 index 000000000..8523ba313 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/65.txt @@ -0,0 +1,26 @@ +### Improvements + +- Disable burgermenu icon animation #1486 +- undo delete of downloads #1472 +- Download option in share menu #1498 +- Added share option to long tap menu #1454 +- Minimize main player on exit #1354 +- Library version update and database backup fix #1510 +- ExoPlayer 2.8.2 Update #1392 + - Reworked the playback speed control dialog to support different step sizes for faster speed change. + - + - + - + - +- +- +- +- +- +- + - + - +- +- + - + -. diff --git a/fastlane/metadata/android/cs/changelogs/66.txt b/fastlane/metadata/android/cs/changelogs/66.txt new file mode 100644 index 000000000..d62f6db4f --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/66.txt @@ -0,0 +1,16 @@ +# Changelog of v0.13.7 + +### Fixed +- Fix sort filter issues of v0.13.6 + +# Changelog of v0.13.6 + +### Improvements + +- Disable burgermenu icon animation #1486 +- undo delete of downloads #1472 +- Download option in share menu #1498 +- Added share option to long tap menu #1454 +- Minimize main player on exit #1354 +- Library version update and database backup fix #1510 +- ExoPlayer 2.8.2 Update #1392 diff --git a/fastlane/metadata/android/cs/changelogs/68.txt b/fastlane/metadata/android/cs/changelogs/68.txt new file mode 100644 index 000000000..528100d34 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/68.txt @@ -0,0 +1,15 @@ +# changes of v0.14.1 + +### Fixed +- Fixed failed to decrypt video url #1659 +- Fixed description link not extract well #1657 + +# changes of v0.14.0 + +### New +- New Drawer design #1461 +- New customizable front page #1461 + +### Improvements +- Reworked Gesture controls #1604 +- New way to close the popup player #1597 diff --git a/fastlane/metadata/android/cs/changelogs/69.txt b/fastlane/metadata/android/cs/changelogs/69.txt new file mode 100644 index 000000000..48113281b --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/69.txt @@ -0,0 +1,9 @@ +### New +- Odstranění a sdílení v odběrech dlouhým klepnutím #1516 +- Uživatelské rozhraní tabletu a rozvržení seznamu mřížky #1617 + +### Vylepšení +- ukládání a opětovné načítání naposledy použitého poměru stran #1748 +- Povolení lineárního rozložení v aktivitě Stahování s úplnými názvy videí #1771 +- Odstraňování a sdílení odběrů přímo z karty odběrů #1516 +- Enqueuing nyní spustí přehrávání videa, pokud fronta přehrávání již skončila #1783 diff --git a/fastlane/metadata/android/cs/changelogs/70.txt b/fastlane/metadata/android/cs/changelogs/70.txt new file mode 100644 index 000000000..6a4f43976 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/70.txt @@ -0,0 +1,7 @@ +UPOZORNĚNÍ: Tato verze je pravděpodobně plná chyb, stejně jako ta předchozí. Nicméně vzhledem k úplnému vypnutí od 17. je lepší rozbitá verze než žádná. Nebo ne? ¯\_(ツ)_/¯ + +### Vylepšení +* stažené soubory lze nyní otevřít jedním kliknutím #1879 +* podpora upuštění pro Android 4.1 - 4.3 #1884 +* odstranění starého přehrávače #1884 +* odstranění streamů z aktuální fronty přehrávání přejetím doprava #1915 diff --git a/fastlane/metadata/android/cs/changelogs/71.txt b/fastlane/metadata/android/cs/changelogs/71.txt new file mode 100644 index 000000000..a0fdaa76f --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/71.txt @@ -0,0 +1,7 @@ +### Vylepšení +* Přidání oznámení o aktualizaci aplikace pro sestavení na GitHubu (#1608 by @krtkush) +* Různá vylepšení downloaderu (#1944 by @kapodamy): + * přidat chybějící bílé ikony a použít hardcorový způsob změny barev ikon + * kontrola, zda je iterátor inicializován (oprava #2031) + * umožnit opakování stahování při chybě "post-processing failed" v novém muxeru + * nový muxer MPEG-4 opravuje nesynchronní video a audio toky (#2039) diff --git a/fastlane/metadata/android/cs/changelogs/730.txt b/fastlane/metadata/android/cs/changelogs/730.txt new file mode 100644 index 000000000..13d0879c8 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/730.txt @@ -0,0 +1,2 @@ +# Fixed +- Znovu opravte chybu funkce dešifrování. diff --git a/fastlane/metadata/android/cs/changelogs/740.txt b/fastlane/metadata/android/cs/changelogs/740.txt new file mode 100644 index 000000000..479d25e25 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/740.txt @@ -0,0 +1,23 @@ +

Improvements

+
    +
  • make links in comments clickable, increase text size
  • +
  • seek on clicking timestamp links in comments
  • +
  • show preferred tab based on recently selected state
  • +
  • add playlist to queue when long clicking on 'Background' in playlist window
  • +
  • search for shared text when it is not an URL
  • +
  • add "share at current time" button to the main video player
  • +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- diff --git a/fastlane/metadata/android/cs/changelogs/750.txt b/fastlane/metadata/android/cs/changelogs/750.txt new file mode 100644 index 000000000..325be0c28 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/750.txt @@ -0,0 +1,14 @@ +New +Playback resume #2288 +• Resume streams where you stopped last time +Downloader Enhancements #2149 +• Use Storage Access Framework to store downloads on external SD-cards +• New mp4 muxer +• Optionally change the download directory before starting a download +• Respect metered networks + + +Improved +• Removed gema strings #2295 +• Handle (auto)rotation changes during activity lifecycle #2444 +• Make long-press menus consistent #2368 diff --git a/fastlane/metadata/android/cs/changelogs/760.txt b/fastlane/metadata/android/cs/changelogs/760.txt new file mode 100644 index 000000000..d449ec53a --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/760.txt @@ -0,0 +1,14 @@ +Změny ve verzi 0.17.1 + +Nové stránky +- Thajská lokalizace + + +Vylepšené stránky +- Znovu přidána akce "začít přehrávat zde" v nabídkách pro dlouhé stisknutí pro seznamy skladeb #2518 +- Přidání přepínače pro výběr souborů SAF / legacy #2521 + +Opraveno +- Oprava mizení tlačítek v zobrazení stahování při přepínání aplikací #2487 +- Oprava pozice přehrávání se ukládá, i když je vypnutá historie sledování +- Oprava sníženého výkonu způsobeného pozicí přehrávání v zobrazeních seznamu #2517 diff --git a/fastlane/metadata/android/cs/changelogs/770.txt b/fastlane/metadata/android/cs/changelogs/770.txt new file mode 100644 index 000000000..0a07f6bdd --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/770.txt @@ -0,0 +1,4 @@ +Změny ve verzi 0.17.2 + +Oprava +- Oprava nebylo k dispozici žádné video diff --git a/fastlane/metadata/android/cs/changelogs/780.txt b/fastlane/metadata/android/cs/changelogs/780.txt new file mode 100644 index 000000000..32cc03d1f --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/780.txt @@ -0,0 +1,11 @@ +Změny ve verzi 0.17.3 + +Vylepšené stránky +- Přidána možnost vymazat stavy přehrávání #2550 +- Zobrazení skrytých adresářů ve výběru souborů #2591 +- Podpora otevírání adres URL z instancí `invidio.us` pomocí NewPipe #2488 +- Přidána podpora pro adresy URL `music.youtube.com` TeamNewPipe/NewPipeExtractor#194 + +Opraveno +- YouTube] Opraveno 'java.lang.IllegalArgumentException #192 +- YouTube] Opraveno nefunkční živé vysílání TeamNewPipe/NewPipeExtractor#195 diff --git a/fastlane/metadata/android/cs/changelogs/790.txt b/fastlane/metadata/android/cs/changelogs/790.txt new file mode 100644 index 000000000..04b5c5763 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/790.txt @@ -0,0 +1,9 @@ +Vylepšené stránky +- Přidání více nadpisů pro zlepšení přístupnosti pro nevidomé #2655 +- Udělejte jazyk nastavení složky pro stahování konzistentnější a méně nejednoznačný #2637 + +Opraveno +- Kontrola, zda je stažen poslední bajt v bloku #2646 +- Opraveno posouvání ve fragmentu detailu videa #2672 +- Odstranění dvojité animace vymazání vyhledávacího pole na jednu #2695 +- [SoundCloud] Oprava extrakce client_id #2745 diff --git a/fastlane/metadata/android/cs/changelogs/800.txt b/fastlane/metadata/android/cs/changelogs/800.txt new file mode 100644 index 000000000..0a22893f1 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/800.txt @@ -0,0 +1,10 @@ +Nový +- Podpora PeerTube bez P2P (#2201) [Beta]: + ◦ Sledování a stahování videí z instancí PeerTube + ◦ Přidání instancí v nastavení pro přístup ke kompletnímu světu PeerTube + ◦ V systémech Android 4.4 a 7.1 mohou být při přístupu k některým instancím problémy s přenosem SSL, což může vést k chybě sítě. + +- Downloader (#2679): + ◦ Vypočítat předpokládaný čas stahování + ◦ Stáhnout opus (soubory webm) jako ogg + ◦ Obnovení vypršených odkazů ke stažení pro obnovení stahování po dlouhé pauze diff --git a/fastlane/metadata/android/cs/changelogs/810.txt b/fastlane/metadata/android/cs/changelogs/810.txt new file mode 100644 index 000000000..c04a9cac9 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/810.txt @@ -0,0 +1,8 @@ +Nový +- Zobrazení miniatury videa na zamykací obrazovce při přehrávání na pozadí + +Vylepšená stránka +- Přidání místního seznamu skladeb do fronty při dlouhém stisknutí tlačítka na pozadí / vyskakovacího tlačítka +- Umožnit posouvání karet hlavní stránky a jejich skrytí, pokud je k dispozici pouze jedna karta +- Omezit počet aktualizací miniatur oznámení v přehrávači na pozadí +- Přidání fiktivní miniatury pro prázdné místní seznamy skladeb diff --git a/fastlane/metadata/android/cs/changelogs/820.txt b/fastlane/metadata/android/cs/changelogs/820.txt new file mode 100644 index 000000000..9dc52c6f5 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/820.txt @@ -0,0 +1 @@ +Opraven regex názvu dešifrovací funkce, který znemožňuje použití služby YouTube. diff --git a/fastlane/metadata/android/cs/changelogs/830.txt b/fastlane/metadata/android/cs/changelogs/830.txt new file mode 100644 index 000000000..1f6666912 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/830.txt @@ -0,0 +1 @@ +Aktualizováno klient_id služby SoundCloud pro opravu problémů se službou SoundCloud. diff --git a/fastlane/metadata/android/cs/changelogs/840.txt b/fastlane/metadata/android/cs/changelogs/840.txt new file mode 100644 index 000000000..73da3bcc5 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/840.txt @@ -0,0 +1,8 @@ +Nový +- Přidán volič jazyka pro změnu jazyka aplikace +- Přidáno tlačítko odeslat do Kodi do skládací nabídky přehrávače +- Přidána možnost kopírování komentářů při dlouhém stisknutí + +Vylepšena stránka +- Oprava aktivity ReCaptcha a správné ukládání získaných souborů cookie +- Odstraněna nabídka s tečkami ve prospěch šuplíku a skrytí tlačítka historie, pokud není v nastavení povolena historie sledování diff --git a/fastlane/metadata/android/cs/changelogs/850.txt b/fastlane/metadata/android/cs/changelogs/850.txt new file mode 100644 index 000000000..86fa0fc0f --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/850.txt @@ -0,0 +1 @@ +V tomto vydání byla aktualizována verze webových stránek YouTube. Stará verze webových stránek bude v březnu ukončena, a proto je nutné provést aktualizaci NewPipe. diff --git a/fastlane/metadata/android/cs/changelogs/860.txt b/fastlane/metadata/android/cs/changelogs/860.txt new file mode 100644 index 000000000..b1d6765f8 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/860.txt @@ -0,0 +1,7 @@ +Vylepšené stránky +- Uložení a obnovení, zda je výška tónu a tempo odpojeno, nebo ne +- Podpora výřezu displeje v přehrávači +- Kulaté zobrazení a počet účastníků +- Optimalizováno pro YouTube tak, aby využívalo méně dat + +V této verzi bylo opraveno více než 15 chyb souvisejících s YouTube. diff --git a/fastlane/metadata/android/cs/changelogs/870.txt b/fastlane/metadata/android/cs/changelogs/870.txt new file mode 100644 index 000000000..27cc41b5d --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/870.txt @@ -0,0 +1,2 @@ +Jedná se o opravnou verzi, která aktualizuje NewPipe tak, aby opět umožňovala používání služby SoundCloud bez větších potíží. +V extraktoru se nyní používá rozhraní API SoundCloud v2 a byla vylepšena detekce neplatných ID klientů. diff --git a/fastlane/metadata/android/cs/changelogs/900.txt b/fastlane/metadata/android/cs/changelogs/900.txt new file mode 100644 index 000000000..301ced059 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/900.txt @@ -0,0 +1,13 @@ +Nový +- Skupiny předplatného a tříděné kanály +- Tlačítko ztlumení zvuku v přehrávačích + +Vylepšené stránky +- Povoleno otevírání odkazů na music.youtube.com a media.ccc.de v aplikaci NewPipe +- Přemístění dvou nastavení ze vzhledu do obsahu +- Skrytí možností vyhledávání po 5, 15 a 25 sekundách, pokud je povoleno nepřesné vyhledávání + +Opraveno +- některá videa WebM nelze zobrazit +- zálohování databáze v systému Android P +- pád při sdílení staženého souboru diff --git a/fastlane/metadata/android/cs/changelogs/910.txt b/fastlane/metadata/android/cs/changelogs/910.txt new file mode 100644 index 000000000..130b5dc87 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/910.txt @@ -0,0 +1 @@ +Opravena migrace databáze, která v některých vzácných případech znemožňovala spuštění aplikace NewPipe. diff --git a/fastlane/metadata/android/cs/changelogs/920.txt b/fastlane/metadata/android/cs/changelogs/920.txt new file mode 100644 index 000000000..b5e3167ee --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/920.txt @@ -0,0 +1,9 @@ +Vylepšeno + +- Přidáno datum nahrání a počet zobrazení na položkách mřížky streamu +- Vylepšení rozvržení záhlaví zásuvky + +Opraveno + +- Opraveno tlačítko ztlumení zvuku způsobující pády na rozhraní API 19 +- Opraveno stahování dlouhých videí 1080p 60fps diff --git a/fastlane/metadata/android/cs/changelogs/930.txt b/fastlane/metadata/android/cs/changelogs/930.txt new file mode 100644 index 000000000..e72a3a61c --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/930.txt @@ -0,0 +1,10 @@ +Nový +- Vyhledávání na YouTube Music +- Základní podpora Android TV + +Vylepšené stránky +- Přidána možnost odstranit všechna sledovaná videa z místního seznamu skladeb +- Zobrazení zprávy, když obsah ještě není podporován, místo pádu. +- Vylepšena změna velikosti vyskakovacího přehrávače pomocí gest štípnutí +- Enqueue streamy při dlouhém stisknutí tlačítek na pozadí a vyskakovacích tlačítek v kanálu +- Vylepšené zpracování velikosti záhlaví zásuvky diff --git a/fastlane/metadata/android/cs/changelogs/940.txt b/fastlane/metadata/android/cs/changelogs/940.txt new file mode 100644 index 000000000..7988003d8 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/940.txt @@ -0,0 +1,9 @@ +Nový +- Přidání podpory pro komentáře SoundCloud +- Přidání nastavení omezeného režimu YouTube +- Zobrazení podrobností o nadřazeném kanálu PeerTube + +Vylepšené stránky +- Zobrazení tlačítka Kore pouze pro podporované služby +- Blokování gest přehrávače, která začínají na panelu NavigationBar nebo StatusBar +- Změna barvy pozadí tlačítek opakování a přihlášení k odběru na základě barvy služby diff --git a/fastlane/metadata/android/cs/changelogs/950.txt b/fastlane/metadata/android/cs/changelogs/950.txt new file mode 100644 index 000000000..21e52a365 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/950.txt @@ -0,0 +1,4 @@ +Tato verze přináší tři drobné opravy: +- Oprava přístupu k úložišti v systému Adroid 10+ +- Opraveno otevírání kiosků +- Opraveno rozbor trvání dlouhých videí diff --git a/fastlane/metadata/android/cs/changelogs/951.txt b/fastlane/metadata/android/cs/changelogs/951.txt new file mode 100644 index 000000000..e5e3b3a64 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/951.txt @@ -0,0 +1,6 @@ +Nový +- Přidání vyhledávání pro výběr odběru v dialogovém okně skupiny kanálů +- Přidání filtru do dialogového okna skupiny kanálů pro zobrazení pouze neseskupených odběrů +- Přidání karty seznamu skladeb na hlavní stránku +- Rychlé převíjení vpřed/vzad ve frontě přehrávačů na pozadí/vyskočení. +- Zobrazení návrhu vyhledávání: mysleli jste a zobrazení výsledku pro diff --git a/fastlane/metadata/android/cs/changelogs/953.txt b/fastlane/metadata/android/cs/changelogs/953.txt new file mode 100644 index 000000000..cb7626862 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/953.txt @@ -0,0 +1 @@ +Oprava extrakce dešifrovací funkce YouTube. diff --git a/fastlane/metadata/android/cs/changelogs/954.txt b/fastlane/metadata/android/cs/changelogs/954.txt new file mode 100644 index 000000000..e07b70aa2 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/954.txt @@ -0,0 +1,6 @@ +- nový pracovní postup aplikace: přehrávání videí na stránce s detailem, přejetí prstem dolů pro minimalizaci přehrávače +- Oznámení MediaStyle: přizpůsobitelné akce v oznámeních, zlepšení výkonu +- základní změna velikosti při používání aplikace NewPipe jako aplikace pro stolní počítače + +- zobrazení dialogu s možnostmi otevření v případě přípitku nepodporované adresy URL +- Zlepšení zkušeností s návrhy vyhledávání, pokud nelze načíst ty vzdálené diff --git a/fastlane/metadata/android/cs/changelogs/955.txt b/fastlane/metadata/android/cs/changelogs/955.txt new file mode 100644 index 000000000..ed158e34b --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/955.txt @@ -0,0 +1,3 @@ +[YouTube] Oprava vyhledávání pro některé uživatele +[YouTube] Oprava náhodných výjimek při dešifrování +[SoundCloud] Adresy URL, které končí lomítkem, jsou nyní zpracovávány správně diff --git a/fastlane/metadata/android/cs/changelogs/956.txt b/fastlane/metadata/android/cs/changelogs/956.txt new file mode 100644 index 000000000..f31882573 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/956.txt @@ -0,0 +1 @@ +[YouTube] Opraveno selhání při načítání jakéhokoli videa diff --git a/fastlane/metadata/android/cs/changelogs/957.txt b/fastlane/metadata/android/cs/changelogs/957.txt new file mode 100644 index 000000000..666c51097 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/957.txt @@ -0,0 +1,8 @@ +- Sjednocení specifických akcí enqueue do jedné +- Gesto dvěma prsty pro zavření přehrávače +- Povolení vymazání souborů cookie reCAPTCHA +- Možnost nezabarvovat oznámení +- Vylepšení způsobu otevírání detailů videa s cílem opravit nekonečné vyrovnávací paměti, chybné chování při sdílení do NewPipe a další nesrovnalosti +- Zrychlení videí na YouTube a oprava videí s věkovým omezením +- Oprava pádu při rychlém převíjení vpřed/vzad +- Nepřeuspořádávat seznamy přetahováním miniatur diff --git a/fastlane/metadata/android/cs/changelogs/958.txt b/fastlane/metadata/android/cs/changelogs/958.txt new file mode 100644 index 000000000..989f9ad61 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/958.txt @@ -0,0 +1,15 @@ +Nové a vylepšené: +- Znovu přidána možnost skrýt miniaturu na zamykací obrazovce +- Tažení pro obnovení kanálu +- Vylepšený výkon při načítání místních seznamů + +Opraveno: +- Opraven pád při spuštění aplikace NewPipe po jejím vyjmutí z paměti RAM +- Opraven pád při spuštění, když není připojení k internetu +- Opraveno: Respektování nastavení jasu a nastavení hlasitosti +- YouTube] Opraveny dlouhé seznamy skladeb + +Ostatní: +- Vyčištění kódu a několik interních vylepšení +- Aktualizace závislostí +- diff --git a/fastlane/metadata/android/cs/changelogs/959.txt b/fastlane/metadata/android/cs/changelogs/959.txt new file mode 100644 index 000000000..18a25645b --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/959.txt @@ -0,0 +1,3 @@ +Opravena nekonečná smyčka pádů po otevření hlášení chyb. +Aktualizován seznam instancí PeerTube, které lze automaticky otevřít pomocí NewPipe. +Aktualizovány překlady. diff --git a/fastlane/metadata/android/cs/changelogs/960.txt b/fastlane/metadata/android/cs/changelogs/960.txt new file mode 100644 index 000000000..c25277f64 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/960.txt @@ -0,0 +1,4 @@ +- Vylepšený popis možnosti exportu databáze v nastavení. +- Opraveno zpracování komentářů na YouTube. +- Opraveno zobrazení názvu služby media.ccc.de. +- Aktualizovány překlady. diff --git a/fastlane/metadata/android/cs/changelogs/961.txt b/fastlane/metadata/android/cs/changelogs/961.txt new file mode 100644 index 000000000..db711f0d4 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/961.txt @@ -0,0 +1,12 @@ +- [YouTube] Podpora mixu +- [YouTube] Zobrazení informací o veřejnoprávních vysílatelích a Covid-19 +- [media.ccc.de] Přidána nejnovější videa +- Přidán somálský překlad + +- Mnoho interních vylepšení + +- Opraveno sdílení videí z přehrávače +- Opraveno prázdné webové zobrazení ReCaptcha +- Opraven pád, ke kterému docházelo při odebírání streamu ze seznamu +- [PeerTube] Opraveny související streamy +- YouTube] Opraveno vyhledávání hudby na YouTube diff --git a/fastlane/metadata/android/cs/changelogs/963.txt b/fastlane/metadata/android/cs/changelogs/963.txt new file mode 100644 index 000000000..e971418af --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/963.txt @@ -0,0 +1 @@ +- [YouTube] Opraveno pokračování kanálu diff --git a/fastlane/metadata/android/cs/changelogs/964.txt b/fastlane/metadata/android/cs/changelogs/964.txt new file mode 100644 index 000000000..11eacbcd8 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/964.txt @@ -0,0 +1,8 @@ +- Přidána podpora kapitol v ovládání hráče +- [PeerTube] Přidáno vyhledávání v sépiové barvě +- Znovu přidáno tlačítko pro sdílení v zobrazení detailu videa a popis streamu přesunut do rozložení karet +- Zakázáno obnovení jasu, pokud je gesto jasu zakázáno +- Přidána položka seznamu pro přehrávání videa v Kodi +- Opraven pád v případě, že na některých zařízeních není nastaven výchozí prohlížeč, a vylepšeny dialogy sdílení +- +- diff --git a/fastlane/metadata/android/cs/changelogs/965.txt b/fastlane/metadata/android/cs/changelogs/965.txt new file mode 100644 index 000000000..c62dfa65c --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/965.txt @@ -0,0 +1,6 @@ +Opraven pád, ke kterému docházelo při změně pořadí skupin kanálů. +Opraveno získávání dalších videí YouTube z kanálů a seznamů skladeb. +Opraveno získávání komentářů YouTube. +Přidána podpora podcest /watch/, /v/ a /w/ v adresách URL YouTube. +Opraveno získávání id klienta služby SoundCloud a obsahu s geografickým omezením. +Přidána lokalizace do severní kurdštiny. diff --git a/fastlane/metadata/android/cs/changelogs/966.txt b/fastlane/metadata/android/cs/changelogs/966.txt new file mode 100644 index 000000000..212687b51 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/966.txt @@ -0,0 +1,14 @@ +Novinka: +- Přidat novou službu: Bandcamp + +Vylepšeno: +- Přidána možnost, aby aplikace následovala motiv zařízení +- Předcházení některým pádům zobrazením vylepšeného panelu chyb +- Zobrazení více informací o tom, proč je obsah nedostupný +- Hardwarové tlačítko mezerníku spouští přehrávání/pauzu +- Zobrazení přípitku "Stahování zahájeno" + +Opraveno: +- Oprava velmi malé miniatury v detailech videa při přehrávání na pozadí +- Oprava prázdného názvu v minimalizovaném přehrávači +- diff --git a/fastlane/metadata/android/cs/changelogs/967.txt b/fastlane/metadata/android/cs/changelogs/967.txt new file mode 100644 index 000000000..ba62e27eb --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/967.txt @@ -0,0 +1 @@ +Opraveno nesprávné fungování služby YouTube v EU. To bylo způsobeno novým systémem souborů cookie a souhlasu s ochranou osobních údajů, který vyžaduje, aby NewPipe nastavil soubor cookie CONSENT. diff --git a/fastlane/metadata/android/cs/changelogs/968.txt b/fastlane/metadata/android/cs/changelogs/968.txt new file mode 100644 index 000000000..5c14d8a5f --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/968.txt @@ -0,0 +1,7 @@ +Do nabídky dlouhého stisknutí tlačítka byla přidána možnost Podrobnosti o kanálu. +Přidána funkce přejmenování názvu seznamu skladeb z rozhraní seznamu skladeb. +Umožňuje uživateli pozastavit video během jeho ukládání do vyrovnávací paměti. +Vyleštěn bílý motiv. +Opraveno překrývání písem při použití větší velikosti písma. +Opraveno chybějící video na zařízeních Formuler a Zephier. +Opraveny různé pády. diff --git a/fastlane/metadata/android/cs/changelogs/969.txt b/fastlane/metadata/android/cs/changelogs/969.txt new file mode 100644 index 000000000..8c5814047 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/969.txt @@ -0,0 +1,8 @@ +- Povolení instalace na externí úložiště +- [Bandcamp] Přidána podpora pro zobrazení prvních tří komentářů u streamu +- Zobrazení přípitku "stahování zahájeno" pouze po zahájení stahování +- Nenastavovat soubor cookie reCaptcha, pokud není uložen žádný soubor cookie +- Přehrávač] Zlepšení výkonu mezipaměti +- Přehrávač] Opraveno automatické nepřehrávání přehrávače +- Zrušit předchozí Snackbary při mazání stahování +- Opraven pokus o odstranění objektu, který není v seznamu diff --git a/fastlane/metadata/android/cs/changelogs/970.txt b/fastlane/metadata/android/cs/changelogs/970.txt new file mode 100644 index 000000000..f526adf8c --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/970.txt @@ -0,0 +1,11 @@ +Nový +- Zobrazení metadat obsahu (značky, kategorie, licence, ...) pod popisem +- Přidána možnost "Zobrazit podrobnosti o kanálu" ve vzdálených (nelokálních) seznamech skladeb +- Přidána možnost "Otevřít v prohlížeči" do nabídky dlouhého stisknutí tlačítka + +Opravena stránka +- Opraven pád při otáčení na stránce s podrobnostmi o videu +- Opraveno tlačítko "Přehrát s Kodi" v přehrávači, které vždy vyzve k instalaci aplikace Kore +- Opraveno a vylepšeno nastavení cest pro import a export +- +- diff --git a/fastlane/metadata/android/cs/changelogs/971.txt b/fastlane/metadata/android/cs/changelogs/971.txt new file mode 100644 index 000000000..481b9edf1 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/971.txt @@ -0,0 +1,3 @@ +Hotfix +- Zvětšení vyrovnávací paměti pro přehrávání po obnovení vyrovnávací paměti +- Opraven pád na tabletech a televizorech při kliknutí na ikonu přehrávání v přehrávači diff --git a/fastlane/metadata/android/cs/changelogs/972.txt b/fastlane/metadata/android/cs/changelogs/972.txt new file mode 100644 index 000000000..d0989c7b9 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/972.txt @@ -0,0 +1,14 @@ +Nový +Rozpoznání časových razítek a hashtagů v popisu +Přidáno ruční nastavení režimu tabletu +Přidána možnost skrýt přehrávané položky ve zdroji + +Vylepšený +Správná podpora rozhraní Storage Access Framework +Lepší zpracování chyb nedostupných a ukončených kanálů +List sdílení Android pro uživatele Androidu 10+ nyní zobrazuje název obsahu. +Aktualizované instance Invidious a podpora předávaných odkazů. + +Stabilní +[YouTube] Obsah s věkovým omezením +- diff --git a/fastlane/metadata/android/cs/changelogs/973.txt b/fastlane/metadata/android/cs/changelogs/973.txt new file mode 100644 index 000000000..2eba9ea7c --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/973.txt @@ -0,0 +1,4 @@ +Hotfix +- Oprava ořezávání miniatur a názvů v mřížkovém rozložení kvůli špatnému výpočtu, kolik videí se vejde do jednoho řádku. +- Oprava dialogu stahování, který zmizí, aniž by cokoli provedl, pokud je otevřen z nabídky sdílení +- Aktualizace knihovny související s otevíráním externích činností, například výběrem souborů v rámci Storage Access Framework diff --git a/fastlane/metadata/android/cs/changelogs/974.txt b/fastlane/metadata/android/cs/changelogs/974.txt new file mode 100644 index 000000000..3149e4737 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/974.txt @@ -0,0 +1,5 @@ +Hotfix +- Oprava problémů s vyrovnávací pamětí způsobených škrcením YouTube +- Oprava extrakce komentářů YouTube a pádů s vypnutými komentáři +- Oprava vyhledávání hudby na YouTube +- Oprava živých přenosů PeerTube diff --git a/fastlane/metadata/android/cs/changelogs/975.txt b/fastlane/metadata/android/cs/changelogs/975.txt new file mode 100644 index 000000000..839bf0e36 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/975.txt @@ -0,0 +1,17 @@ +Nový +- Zobrazení náhledu miniatur při hledání +- Rozpoznání zakázaných komentářů +- Umožňuje označit položku kanálu jako sledovanou +- Zobrazit srdíčka komentářů + +Vylepšené stránky +- Vylepšení rozvržení metadat a značek +- Použití barvy služby na součásti uživatelského rozhraní + +Opraveno +- Oprava miniatur v mini přehrávači +- Oprava nekonečného vyrovnávací paměti u duplicitních položek fronty +- Opravy některých přehrávačů, jako je otáčení a rychlejší zavírání +- +- +- diff --git a/fastlane/metadata/android/cs/changelogs/976.txt b/fastlane/metadata/android/cs/changelogs/976.txt new file mode 100644 index 000000000..cc28400f5 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/976.txt @@ -0,0 +1,10 @@ +- Přidána možnost přímého otevření přehrávače ve fullscreenu +- Umožňuje vybrat, které typy návrhů vyhledávání se mají zobrazit +- Tmavé téma je nyní tmavší + přidána tmavá úvodní obrazovka +- Vylepšený nástroj pro výběr souborů, který šedě označuje nechtěné soubory +- Opraven import odběrů YouTube +- Opakované přehrávání streamu vyžaduje opětovné klepnutí na tlačítko přehrávání +- Opraveno ukončení zvukové relace +- +- +-. diff --git a/fastlane/metadata/android/cs/changelogs/977.txt b/fastlane/metadata/android/cs/changelogs/977.txt new file mode 100644 index 000000000..3418d9d43 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/977.txt @@ -0,0 +1,10 @@ +- Do nabídky dlouhého stisku bylo přidáno tlačítko "přehrát další". +- Do filtru záměrů byla přidána předpona cesty ke krátkým filmům YouTube +- Opraven import nastavení +- Výměna pozice panelu vyhledávání s tlačítky přehrávače na obrazovce Fronta +- Různé opravy související se správcem MediasessionManager +- Opraveno nedokončení panelu vyhledávání po skončení videa +- Zakázáno tunelování médií na RealtekATV +- Rozšířena klikatelná oblast minimalizovaných tlačítek přehrávače + +-. diff --git a/fastlane/metadata/android/cs/changelogs/978.txt b/fastlane/metadata/android/cs/changelogs/978.txt new file mode 100644 index 000000000..caaf1ac57 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/978.txt @@ -0,0 +1 @@ +Opraveno provádění kontroly nové verze NewPipe. Tato kontrola se někdy prováděla příliš brzy, a proto vedla k pádu aplikace. To by nyní mělo být opraveno. diff --git a/fastlane/metadata/android/cs/changelogs/979.txt b/fastlane/metadata/android/cs/changelogs/979.txt new file mode 100644 index 000000000..8d3ee1492 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/979.txt @@ -0,0 +1,2 @@ +- Opraveno obnovení přehrávání +- Vylepšení zajišťující, že služba, která určuje, zda má NewPipe kontrolovat nové verze, není spuštěna na pozadí diff --git a/fastlane/metadata/android/cs/changelogs/980.txt b/fastlane/metadata/android/cs/changelogs/980.txt new file mode 100644 index 000000000..59139b3a2 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/980.txt @@ -0,0 +1,13 @@ +Nový +- Přidání možnosti "Přidat do seznamu skladeb" do nabídky sdílení +- Přidána podpora pro krátké odkazy na y2u.be a PeerTube + +Vylepšené stránky +- Kompaktnější ovládání rychlosti přehrávání +- Kanál nyní zvýrazňuje nové položky +- Možnost "Zobrazit sledované položky" ve feedu je nyní uložena + +Opraveno +- Opravena extrakce lajků a dislajků na YouTube +- Opraveno automatické přehrávání po návratu z pozadí +A mnoho dalšího diff --git a/fastlane/metadata/android/cs/changelogs/981.txt b/fastlane/metadata/android/cs/changelogs/981.txt new file mode 100644 index 000000000..6dd389fd7 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/981.txt @@ -0,0 +1,2 @@ +Odstraněna podpora MediaParser, aby se opravilo selhání obnovení přehrávání po vyrovnávací paměti v systému Android 11+. +Zakázáno tunelování médií na přehrávači Philips QM16XE, aby se odstranily problémy s přehráváním. diff --git a/fastlane/metadata/android/cs/changelogs/982.txt b/fastlane/metadata/android/cs/changelogs/982.txt new file mode 100644 index 000000000..c666499a4 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/982.txt @@ -0,0 +1 @@ +Opraveno nepřehrávání jakéhokoli streamu ve službě YouTube. diff --git a/fastlane/metadata/android/cs/changelogs/983.txt b/fastlane/metadata/android/cs/changelogs/983.txt new file mode 100644 index 000000000..004ccca65 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/983.txt @@ -0,0 +1,9 @@ +Přidání nového uživatelského rozhraní a chování při hledání dvojitým klepnutím +Možnost vyhledávání v nastavení +Zvýraznění připnutých komentářů jako takových +Přidat podporu open-with-app pro instanci FSFE PeerTube +Přidat oznámení o chybách +Oprava přehrávání první položky fronty při změně hráče +Při vyrovnávací paměti během živých přenosů čekat déle, než dojde k selhání +Oprava pořadí výsledků místního vyhledávání +Oprava prázdných políček položek ve frontě přehrávání diff --git a/fastlane/metadata/android/cs/changelogs/984.txt b/fastlane/metadata/android/cs/changelogs/984.txt new file mode 100644 index 000000000..3b9eb35a4 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/984.txt @@ -0,0 +1,7 @@ +načtení dostatečného množství počátečních položek v seznamech, aby zaplnily celou obrazovku, a oprava posouvání na tabletech a televizorech. +Oprava náhodných pádů při procházení seznamů +Nechat překryvný oblouk rychlého vyhledávání hráče přejít pod uživatelské rozhraní systému +Vrátit změny výřezů při přehrávání ve více oknech, které způsobovaly regresi chybně umístěného přehrávače na některých telefonech +Zvýšit compileSdk z 30 na 31 +Aktualizovat knihovnu pro hlášení chyb +- diff --git a/fastlane/metadata/android/cs/changelogs/985.txt b/fastlane/metadata/android/cs/changelogs/985.txt new file mode 100644 index 000000000..7035a1112 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/985.txt @@ -0,0 +1 @@ +Opraveno nepřehrávání jakéhokoli streamu ve službě YouTube diff --git a/fastlane/metadata/android/cs/changelogs/986.txt b/fastlane/metadata/android/cs/changelogs/986.txt new file mode 100644 index 000000000..0ffe8dabc --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/986.txt @@ -0,0 +1,16 @@ +Nový +• Oznámení o nových streamech +• Bezproblémový přechod mezi přehrávači na pozadí a videem +• Změna výšky tónu podle půltónů +• Připojení fronty hlavního přehrávače k seznamu skladeb + +Vylepšený +• Zapamatujte si velikost kroku rychlosti / stoupání +• Zmírnění počátečního dlouhého ukládání do vyrovnávací paměti v přehrávači videa +• Vylepšete uživatelské rozhraní přehrávače pro Android TV +• Potvrďte před odstraněním všech stažených souborů + +Stabilní +• +• +• diff --git a/fastlane/metadata/android/cs/changelogs/987.txt b/fastlane/metadata/android/cs/changelogs/987.txt new file mode 100644 index 000000000..51cd846c2 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/987.txt @@ -0,0 +1,12 @@ +Nový +- Podpora jiných způsobů doručování než progresivního HTTP: rychlejší načítání přehrávání, opravy pro PeerTube a SoundCloud, přehrávání nedávno ukončených živých přenosů na YouTube. +- Tlačítko Přidat pro přidání vzdáleného seznamu skladeb do místního seznamu skladeb +- Náhled obrázku ve sdíleném listu systému Android 10+ + +Vylepšená stránka +- Vylepšení dialogového okna s parametry přehrávání +- Přesunutí tlačítek pro import/export předplatného do nabídky se třemi tečkami + +Opraveno +- +- diff --git a/fastlane/metadata/android/cs/changelogs/988.txt b/fastlane/metadata/android/cs/changelogs/988.txt new file mode 100644 index 000000000..76da8121c --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] Oprava chyby "Nelze načíst žádný stream" při pokusu o přehrání jakéhokoli videa +[YouTube] Oprava zprávy "Následující obsah není v této aplikaci k dispozici." zobrazené místo požadovaného videa diff --git a/fastlane/metadata/android/cs/changelogs/989.txt b/fastlane/metadata/android/cs/changelogs/989.txt new file mode 100644 index 000000000..08ce8dd95 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/989.txt @@ -0,0 +1,3 @@ +• [YouTube] Oprava nekonečného načítání při pokusu přehrát jakékoli video +• [YouTube] Oprava omezování výkonu u některých videí +• Aktualizace knihovny jsoup na verzi 1.15.3, která obsahuje bezpečnostní opravu diff --git a/fastlane/metadata/android/cs/changelogs/990.txt b/fastlane/metadata/android/cs/changelogs/990.txt new file mode 100644 index 000000000..f3145df07 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/990.txt @@ -0,0 +1,14 @@ +V této verzi byla zrušena podpora Androidu 4.4 KitKat, nyní je minimální verzí Android 5 Lollipop! + +Nové +• Stahování z nabídky dlouhého stisknutí +• Skrytí nadcházejících videí ve zdroji +• Sdílení místních seznamů skladeb + +Vylepšení +• Přepracování kódu přehrávače na malé komponenty: menší využití RAM, méně chyb +• Vylepšení režimu měřítka miniatur +• Vektorizace zástupných symbolů obrázků + +Opravy +• Oprava různých problémů s oznámeními: neaktuální/chybějící informace o médiích, zkreslené miniatury diff --git a/fastlane/metadata/android/de/changelogs/987.txt b/fastlane/metadata/android/de/changelogs/987.txt index b6870154c..a857b1caa 100644 --- a/fastlane/metadata/android/de/changelogs/987.txt +++ b/fastlane/metadata/android/de/changelogs/987.txt @@ -9,4 +9,4 @@ Verbesserte Behoben - Fix: Entfernen vollständig angesehener Videos aus der Wiedergabeliste -- Repariert das Thema des Freigabemenüs und den Eintrag "Zur Wiedergabeliste hinzufügen". +- Repariert das Thema des Freigabemenüs und den Eintrag "Zur Wiedergabeliste hinzufügen" diff --git a/fastlane/metadata/android/en-US/changelogs/990.txt b/fastlane/metadata/android/en-US/changelogs/990.txt new file mode 100644 index 000000000..e12c20ba5 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/990.txt @@ -0,0 +1,15 @@ +This release drops support for Android 4.4 KitKat, now the minimum version is Android 5 Lollipop! + +New +• Download from long-press menu +• Hide future videos in feed +• Share local playlists + +Improved +• Refactor the player code into small components: less RAM used, less bugs +• Improve thumbnails' scale mode +• Vector-ize image placeholders + +Fixed +• Fix various issues with the player notification: outdated/missing media info, distorted thumbnail +• Fix fullscreen using 1/4 of screen diff --git a/fastlane/metadata/android/es/changelogs/69.txt b/fastlane/metadata/android/es/changelogs/69.txt new file mode 100644 index 000000000..a2d1c8395 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/69.txt @@ -0,0 +1,19 @@ +### Nuevo +- Mantén pulsado para borrar y/o compartir en Subscripciones #1516 +- Interfaz de Tablet y listas en forma de cuadrícula #1617 + +### Mejoras +- Guarda y usa la última relación de aspecto #1748 +- Activa las listas lineares en las Descargas con los nombres completos #1771 +- Guarda y comparte subscripciones directamente desde la pestaña de subscripciones #1516 +- Poner en cola un video hace que empiece a reproducirse si la cola ya ha acabado #1783 +- Ajustes para gestos separados del el brillo y el volumen #1644 +- Añadido soporte para traducciones #1792 + +### Arreglos +- Arreglada la obtención de fecha para el .format, de modo que NewPipe se puede usar en Finlandia. +- Arreglado el contador de subscriptores +- Añadido permiso para arrancar en primer plano para dispositivos con API +28 #1830 + +### Bugs Conocidos +- El estado de la reproducción no se puede guardar en Android P diff --git a/fastlane/metadata/android/es/changelogs/70.txt b/fastlane/metadata/android/es/changelogs/70.txt new file mode 100644 index 000000000..b5cd027a7 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/70.txt @@ -0,0 +1,25 @@ +ATENCIÓN: Esta versión quizá sea un festival de errores, como la anterior. Sin embargo, debido al cierre total desde la 17. una versión rota es mejor que ninguna versión. ¿Cierto? ¯\_(ツ)_/¯ + +### Mejorías +* los archivos descargados ahora pueden ser abiertos con un solo clic #1879 +* descenso de soporte para android 4.1 - 4.3 #1884 +* eliminar el reproductor antiguo #1884 +* eliminar los flujos de la cola de reproducción actual deslizándolos hacia la derecha #1915 +* eliminar cola de reproducción automática cuando se pone en cola una nueva secuencia manualmente #1878 +* Posprocesamiento para descargas e implementación de características faltantes #1759 por @kapodamy + * Infraestructura de posprocesamiento + * Infraestructura de manejo de errores adecuada (para el descargador) + * Cola en lugar de descargas múltiples +* Mover las descargas pendientes serializadas (archivos `.giga`) hacia datos de aplicación + * Implementar el reintento máximo de descarga + * Pausa adecuada de descargas multihilo +* Detener las descargas cuando se cambia hacia red móvil (nunca funciona, ver 2º punto) +* Guardar el conteo de hilos para las próximas descargas + * Un montón de incoherencias corregidas + +### Corregidos +* Arreglado el fallo con la resolución por defecto ajustada a la mejor y resolución de datos móviles limitada #1835 +* Arreglado el fallo del reproductor emergente #1874 +* NPE al intentar abrir el reproductor en segundo plano #1901 +* Corrección de la inserción de nuevos flujos cuando la cola automática está activada #1878 +* Corregido el problema de descifrado de shuttown diff --git a/fastlane/metadata/android/es/changelogs/740.txt b/fastlane/metadata/android/es/changelogs/740.txt new file mode 100644 index 000000000..6d89a2246 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/740.txt @@ -0,0 +1,23 @@ +

    Mejorías

    +
      +
    • hacer cliqueables enlaces en comentarios, aumentar el tamaño del texto
    • +
    • buscar al hacer clic en enlaces de marca de tiempo en comentarios
    • +
    • mostrar pestaña preferida según el estado seleccionado recientemente
    • +
    • añadir lista de reproducción a cola cuando se hace un clic largo en 'Fondo' en ventana de lista de reproducción
    • +
    • buscar texto compartido cuando no es una URL
    • +
    • añadir botón "compartir en el momento actual" al reproductor de vídeo principal
    • +
    • añadir botón de cierre a reproductor principal cuando la cola de vídeo haya terminado
    • +
    • añadir "Reproducir directamente en segundo plano" a menú de pulsación larga para elementos lista de vídeos
    • +
    • mejorar traducciones a inglés de comandos Reproducir/PonerEnCola
    • +
    • pequeñas mejorías de rendimiento
    • +
    • eliminar archivos no utilizados
    • +
    • actualizar ExoPlayer a 2.9.6
    • +
    • añadir soporte para enlaces Invidious
    • +
    +

    Arreglado

    +
      +
    • arreglado desplazamiento con comentarios y flujos relacionados desactivados
    • +
    • arreglado que TareaBuscarNuevaVersiónDeApp se ejecute cuando no debería
    • +
    • corregida la importación de suscripciones a YouTube: ignorar las que tienen URL inválida y mantener las que tienen el título vacío
    • +arreglar URL inválida de YouTube: nombre de etiqueta de firma no es siempre "firma", lo que impide cargar flujos +
    diff --git a/fastlane/metadata/android/es/changelogs/810.txt b/fastlane/metadata/android/es/changelogs/810.txt new file mode 100644 index 000000000..2f569dc5b --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/810.txt @@ -0,0 +1,19 @@ +Nuevo +- Mostrar la miniatura del vídeo en la pantalla de bloqueo cuando se reproduce en segundo plano + +Mejorado +- Añadir la lista de reproducción local a la cola cuando se hace una pulsación larga en el botón de fondo / emergente +- Hacer que las pestañas de la página principal se puedan desplazar y ocultar cuando sólo hay una pestaña +- Limitar la cantidad de actualizaciones de miniaturas de notificación en el reproductor de fondo +- Añadir una miniatura ficticia para listas de reproducción locales vacías +- Usar la extensión de archivos *.opus en lugar de *.webm y mostrar "opus" en etiqueta de formato en lugar de "WebM Opus" en menú desplegable de descargas +- Añadir un botón para eliminar archivos descargados o el historial de descargas en "Descargas" +- YouTube] Añadir soporte a los enlaces de canal /c/shortened_url + +Corregidos +- Corregidos múltiples problemas al compartir un video a NewPipe y al descargar sus secuencias directamente +- Corregido el acceso al reproductor fuera de su hilo de creación +- Corregida la paginación de resultados de búsqueda +- YouTube] Corregido el cambio a nulo que causaba NPE +- YouTube] Corregida la visualización de comentarios al abrir una url de invidio.us +- SoundCloud] Actualizado client_id diff --git a/fastlane/metadata/android/es/changelogs/840.txt b/fastlane/metadata/android/es/changelogs/840.txt new file mode 100644 index 000000000..37c0628a8 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/840.txt @@ -0,0 +1,24 @@ +Nuevo +- Se ha añadido un selector de idioma para cambiar el idioma de la aplicación +- Se ha añadido el botón de enviar a Kodi al menú desplegable del reproductor +- Se ha añadido la posibilidad de copiar comentarios con una pulsación larga + +Mejorado +- Se ha corregido la actividad de ReCaptcha y se han guardado correctamente las cookies obtenidas +- Menú de puntos eliminado en favor de cajón y botón del historial ocultado cuando no está habilitado el historial de reloj en ajustes +- Pedir permiso de visualización sobre otras aplicaciones en ajustes correctamente en Android 6 y posteriores +- Cambiar nombre de lista de reproducción local haciendo un clic largo en MarcadorDePáginasFragmentos +- Varias mejorías en PeerTube +- Mejoría de varias cadenas de origen en inglés + +Corregido +Corregido que reproductor se reinicie aunque esté en pausa con opción "minimizar al cambiar de app" activada y NewPipe está minimizado +- Corregido el valor de brillo inicial para el gesto +- Corregida la descarga de subtítulos .srt que no contienen todos los saltos de línea +- Corregida descarga a tarjeta SD que falla porque algunos dispositivos Android 5 no son compatibles con CTF +- Corregida la descarga en Android KitKat +- Arreglado el archivo de vídeo .mp4 corrupto que era reconocido como archivo de audio +- Corregidos múltiples problemas de localización, incluyendo códigos de idioma chino erróneos +- YouTube] Las marcas de tiempo en la descripción vuelven a ser cliqueables + +Traducción realizada con la versión gratuita del traductor www.DeepL.com/Translator diff --git a/fastlane/metadata/android/es/changelogs/900.txt b/fastlane/metadata/android/es/changelogs/900.txt new file mode 100644 index 000000000..6ae175a17 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/900.txt @@ -0,0 +1,14 @@ +Nuevo +- Grupos de suscriptores y feeds ordenados +- Botón de silencio en reproductores + +Mejora de +- Permitir la apertura de enlaces music.youtube.com y media.ccc.de en NewPipe +- Reubicar dos ajustes de Apariencia a Contenido +- Ocultar opciones de búsqueda de 5, 15 y 25 seg. si activada búsqueda inexacta + +Corregido +- algunos vídeos WebM no pueden ser buscados +- copia de seguridad de base de datos en Android P +- caída al compartir un archivo descargado +- montones de problema de extracción de YouTube y más ... diff --git a/fastlane/metadata/android/es/changelogs/930.txt b/fastlane/metadata/android/es/changelogs/930.txt new file mode 100644 index 000000000..65cd48b98 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/930.txt @@ -0,0 +1,19 @@ +Nuevo +- Búsqueda en YouTube Music +- Soporte básico de Android TV + +Mejorías de +- Añadida la opción de borrar todos los vídeos vistos de lista de reproducción local +- Mostrar mensaje cuando el contenido aún no es compatible en lugar de caída +- Mejora del tamaño del reproductor emergente con gestos de pellizco +- Puesta en cola de flujos con presión prolongada de botones de fondo y emergentes en el canal +- Mejora de la gestión del tamaño del título de la cabecera de cajón + +Corregido +- Arreglado ajuste restringido de contenido por edad que no funciona +- Corregidos ciertos tipos de reCAPTCHAs +- Fallo corregido al abrir marcadores mientras lista de reproducción es "nula". +- Corregida la detección de excepciones relacionadas con la red +- Corregida visibilidad de botón de clasificación de grupos en fragmento de suscripciones + +y más diff --git a/fastlane/metadata/android/es/changelogs/940.txt b/fastlane/metadata/android/es/changelogs/940.txt new file mode 100644 index 000000000..41a09c1a0 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/940.txt @@ -0,0 +1,16 @@ +Nuevos +- Añadir soporte para los comentarios de SoundCloud +- Añadir la configuración del modo restringido de YouTube +- Mostrar los detalles del canal padre de PeerTube + +Mejora de +- Mostrar el botón Kore sólo para los servicios compatibles +- Bloquear gestos de reproductor que inician en NavigationBar o StatusBar +- Cambio color fondo de botones reintento y suscripción según color de servicio + +Arreglados +- Corregido el congelamiento del diálogo de descarga +- El botón "Abrir en navegador" ahora se abre realmente en navegador +- Arreglo de colapso al abrir vídeos y "No se pudo reproducir este flujo" + +y más diff --git a/fastlane/metadata/android/es/changelogs/964.txt b/fastlane/metadata/android/es/changelogs/964.txt new file mode 100644 index 000000000..8de16d21a --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/964.txt @@ -0,0 +1,8 @@ +- Soporte añadido para capítulos en controles del reproductor +- [PeerTube] Añadida la búsqueda de Sepia +- Botón compartir reañadido en vista detalles de vídeo y descripción de secuencia movida a diseño de pestaña +- Desactivar restauración de brillo si gesto de brillo desactivado +- Añadido el elemento de lista para reproducir vídeo en kodi +- Fallo corregido si no hay navegador por defecto en ciertos dispositivos, diálogos compartir mejorados +- Alternar reproducción/pausa con botón de espacio de hardware en reproductor de pantalla completa +- [media.ccc.de] Varias correcciones y mejoras diff --git a/fastlane/metadata/android/es/changelogs/966.txt b/fastlane/metadata/android/es/changelogs/966.txt new file mode 100644 index 000000000..0cb20696c --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/966.txt @@ -0,0 +1,14 @@ +Nuevo: +- Añadir un nuevo servicio: Bandcamp + +Mejorados: +- Añadir una opción para que la app siga el tema del dispositivo +- Evitar algunos colapsos mostrando un panel de error mejorado +- Mostrar más información sobre razón de contenido no disponible +- Botón de espacio de hardware activa reproducción/pausa +- Mostrar brindis de "Descarga iniciada" + +Corregidos: +- Arreglar miniatura chica en detalles de vídeo durante reproducción de fondo +- Arreglar título vacío en el reproductor minimizado +- Arreglar último modo de redimensionamiento no restaurable correctamente diff --git a/fastlane/metadata/android/es/changelogs/968.txt b/fastlane/metadata/android/es/changelogs/968.txt new file mode 100644 index 000000000..6ef82f8d9 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/968.txt @@ -0,0 +1,7 @@ +Opción de detalles de canal añadida a menú de pulsación larga. +Función añadida de cambiar Nombre de Lista de Reproducción desde su interfaz. +Permitir al usuario pausar video almacenando en memoria intermedia. +Se ha pulido el tema blanco. +Solapamiento corregido de fuentes al usar un tamaño mayor de fuente. +Ausencia de video corregida en dispositivos Formuler y Zephier. +Se han corregido varios fallos. diff --git a/fastlane/metadata/android/es/changelogs/969.txt b/fastlane/metadata/android/es/changelogs/969.txt new file mode 100644 index 000000000..7714b1aef --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/969.txt @@ -0,0 +1,8 @@ +- Permitir instalación en almacenamiento externo +- Bandcamp] Soporte añadido para mostrar 3 primeros comentarios en una secuencia +- Sólo mostrar brindis de "descarga iniciada" cuando la descarga inicia +- No establecer la cookie reCaptcha si no hay cookies almacenadas +- [Reproductor] Mejorar el rendimiento de la caché +- [Reproductor] Arreglado reproductor sin reproducción automática +- Descartar Snackbars anteriores al borrar descargas +- Corregido intento de eliminar objetos fuera de lista diff --git a/fastlane/metadata/android/es/changelogs/970.txt b/fastlane/metadata/android/es/changelogs/970.txt new file mode 100644 index 000000000..39bb2e23e --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/970.txt @@ -0,0 +1,11 @@ +Nuevos +- Mostrar metadatos de contenido (etiquetas, categorías, licencia, ...) bajo descripción +- Opción añadida "Mostrar detalles de canal" en listas de reproducción remotas (no locales) +- Opción añadida "Abrir en navegador" en menú de pulsación larga + +Corregidos +- Fallo corregido de rotación en la página de detalles de vídeo +- Botón corregido "Reproducir con Kodi" en reproductor que siempre pide instalar Kore +- Corregido y mejorado ajuste de rutas de importación y exportación +- [YouTube] Corregido el recuento de comentarios preferidos +Y mucho más diff --git a/fastlane/metadata/android/es/changelogs/973.txt b/fastlane/metadata/android/es/changelogs/973.txt new file mode 100644 index 000000000..09576bc99 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/973.txt @@ -0,0 +1,4 @@ +Corrección en caliente +- Corrección de miniaturas y títulos recortados en diseño de cuadrícula, por cálculo erróneo de cuántos vídeos caben en 1 fila +- Corrección de diálogo de descarga que desaparece sin hacer nada si se abre desde menú compartir +- Actualización de biblioteca relacionada con apertura de actividades externas, como selector de archivos de Marco de Acceso a Almacenamiento diff --git a/fastlane/metadata/android/es/changelogs/980.txt b/fastlane/metadata/android/es/changelogs/980.txt new file mode 100644 index 000000000..b33810479 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/980.txt @@ -0,0 +1,13 @@ +Nuevos +- Opción añadida "Añadir a lista de reproducción" a menú compartir +- Soporte añadido para enlaces cortos de y2u.be y PeerTube + +Mejorados +- Controles de velocidad de reproducción más compactos +- Feed destaca ahora nuevos elementos +- Ahora se guarda la opción "Mostrar elementos vistos" en feed + +Corregidos +- Extracción corregida de "likes" y "dislikes" de YouTube +- Repetición automática corregida después de volver del fondo +Y mucho más diff --git a/fastlane/metadata/android/es/changelogs/982.txt b/fastlane/metadata/android/es/changelogs/982.txt new file mode 100644 index 000000000..e38ba90c9 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/982.txt @@ -0,0 +1 @@ +Solución a YouTube no reproduciendo flujos. diff --git a/fastlane/metadata/android/es/changelogs/984.txt b/fastlane/metadata/android/es/changelogs/984.txt new file mode 100644 index 000000000..3460b9754 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/984.txt @@ -0,0 +1,7 @@ +Carga suficientes elementos iniciales en listas para llenar pantalla entera y arreglo desplazamiento en tabletas y televisores +Arreglar fallos aleatorios al desplazarse por las listas +Hacer que arco de superposición de búsqueda rápida de reproductor vaya bajo la IU de sistema +Revertir cambios en cortes al reproducir en multiventana, que causan regresión de reproductor desubicado en teléfonos +Aumentar compileSdk de 30 a 31 +Actualizar la biblioteca de informes de errores +Refactorizar algunos códigos en reproductor diff --git a/fastlane/metadata/android/es/changelogs/985.txt b/fastlane/metadata/android/es/changelogs/985.txt new file mode 100644 index 000000000..80b4efa55 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/985.txt @@ -0,0 +1 @@ +Arreglo en YouTube no reproduciendo flujos diff --git a/fastlane/metadata/android/es/changelogs/986.txt b/fastlane/metadata/android/es/changelogs/986.txt new file mode 100644 index 000000000..1631b5d81 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/986.txt @@ -0,0 +1,16 @@ +Nuevos +- Notificaciones de nuevos flujos +- Transición perfecta entre fondo y reproductores de vídeo +- Cambio de tono por semitonos +- Añadir cola de reproductor principal a lista de reproducción + +Mejorías +- Recordar el tamaño del paso de velocidad/tono +- Mitigar el largo buffering inicial en el reproductor de vídeo +- Mejor interfaz de usuario de reproductor para Android TV +- Confirmar antes de borrar todos los archivos descargados + +Corregidos +- Arreglar botón multimedia no oculta controles de reproductor +- Corregir reinicio reproducción al cambiar tipo de reproductor +- Arreglar rotación de diálogo de lista de reproducción diff --git a/fastlane/metadata/android/es/changelogs/987.txt b/fastlane/metadata/android/es/changelogs/987.txt new file mode 100644 index 000000000..bdad4e10d --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/987.txt @@ -0,0 +1,12 @@ +Nuevos +- Soporta métodos de entrega distintos a HTTP progresivo: tiempo más rápido de carga de reproducción, arreglos PeerTube y SoundCloud, reproducción de livestreams YouTube recién terminados +- Botón para añadir una lista de reproducción remota a una local +- Vista previa de imagen en hoja de compartir de Android 10+ + +Mejorías +- Mejorar diálogo de parámetros de reproducción +- Mover botones de importación/exportación de suscripciones a menú de 3 puntos + +Arreglados +- Arreglar eliminación de vídeos totalmente vistos de lista de reproducción +- Tema corregido de menú compartir y entrada "añadir a lista de reproducción diff --git a/fastlane/metadata/android/es/changelogs/988.txt b/fastlane/metadata/android/es/changelogs/988.txt new file mode 100644 index 000000000..0e1a88e6f --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] Arreglo error "No se pudo obtener flujo" al intentar reproducir videos +[YouTube] Arreglo el mensaje "Siguiente contenido no disponible en esta aplicación" en lugar de video solicitado diff --git a/fastlane/metadata/android/es/changelogs/989.txt b/fastlane/metadata/android/es/changelogs/989.txt new file mode 100644 index 000000000..72f3a8098 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/989.txt @@ -0,0 +1,3 @@ +- YouTube] Arreglo carga infinita al tratar de reproducir videos +- YouTube] Arreglo de ralentización de algunos vídeos +- Actualización de biblioteca jsoup a versión 1.15.3, con un arreglo de seguridad diff --git a/fastlane/metadata/android/es/changelogs/990.txt b/fastlane/metadata/android/es/changelogs/990.txt new file mode 100644 index 000000000..217ceaaa9 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/990.txt @@ -0,0 +1,15 @@ +Esta versión deja de soportar Android 4.4 KitKat, ¡ahora la versión mínima es Android 5 Lollipop! + +Nuevos +- Descarga desde menú de pulsación larga +- Ocultar futuros vídeos en feed +- Compartir listas de reproducción locales + +Mejorados +- Refactorización de código de reproductor en componentes pequeños: menos RAM usada, menos errores +- Mejorar el modo de escala de miniaturas +- Vectorizar marcadores de posición de imágenes + +Corregidos +- Arreglos varios con notificación de reproductor: antigua/falta información de medios, miniatura distorsionada +- Arreglo pantalla completa usa 1/4 de pantalla diff --git a/fastlane/metadata/android/fr/changelogs/63.txt b/fastlane/metadata/android/fr/changelogs/63.txt index be078632b..b9abcd760 100644 --- a/fastlane/metadata/android/fr/changelogs/63.txt +++ b/fastlane/metadata/android/fr/changelogs/63.txt @@ -1,8 +1,8 @@ ### Améliorations -- Importation/exportation des paramètres #1333 +- Import/export des paramètres #1333 - Réduction overdraw (amélioration des performances) #1371 - Petites améliorations du code #1375 -- GDPR #1420 +- Ajout d'un popup RGPD #1420 ### Corrections - Téléchargeur : Correction d'un plantage lors du chargement de téléchargements inachevés de fichiers .giga #1407 diff --git a/fastlane/metadata/android/fr/changelogs/65.txt b/fastlane/metadata/android/fr/changelogs/65.txt new file mode 100644 index 000000000..bb664a3cb --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/65.txt @@ -0,0 +1,26 @@ +### Améliorations + +- L'animation de l'icône du burgermenu a été désactivé #1486 +- Annulation de la suppression des téléchargements #1472 +- Option de téléchargement dans le menu de partage #1498 +- Ajout d'une option de partage dans le menu "long tap" #1454 +- Réduction du lecteur principal à la sortie #1354 +- Mise à jour de la version de la bibliothèque et correction de la sauvegarde de la base de données #1510 +- Mise à jour de ExoPlayer 2.8.2 #1392 + - La boîte de dialogue de contrôle de la vitesse de lecture a été retravaillée pour prendre en charge différentes tailles de pas pour un changement de vitesse plus rapide. + - Ajout d'une option d'avance rapide pendant les silences dans le contrôle de la vitesse de lecture. Cela devrait être utile pour les livres audio et certains genres musicaux, et peut apporter une véritable expérience transparente (et peut casser une chanson avec beaucoup de silences =\\). + - Refonte de la résolution des sources de médias pour permettre le passage des métadonnées avec les médias en interne dans le lecteur, plutôt que de le faire manuellement. Maintenant, nous avons une seule source de métadonnées et elles sont directement disponibles lorsque la lecture commence. + - Correction des métadonnées des listes de lecture distantes qui ne sont pas mises à jour lorsque de nouvelles métadonnées sont disponibles lors de l'ouverture du fragment de liste de lecture. + - Diverses corrections de l'interface utilisateur : #1383, les contrôles de notification du lecteur en arrière-plan sont maintenant toujours blancs, il est plus facile de fermer le lecteur popup en le lançant. +- Utilisation d'un nouvel extracteur avec une architecture remaniée pour le multiservice. + +### Corrections + +- Correction #1440 Disposition des informations vidéo cassée #1491 +- Correction de l'historique des vues #1497 + - #1495, en mettant à jour les métadonnées (vignette, titre et nombre de vidéos) dès que l'utilisateur accède à la liste de lecture. + - #1475, en enregistrant une vue dans la base de données lorsque l'utilisateur lance une vidéo sur un lecteur externe sur le fragment de détail. +- Correction du timeout de la fenêtre en cas de mode popup. #1463 (Corrigé #640) +- Correction du lecteur vidéo principal #1509 + - Correction du mode répétition entraînant un NPE du lecteur lorsqu'une nouvelle intention est reçue alors que l'activité du lecteur est en arrière-plan. + - Correction de la réduction du lecteur en popup ne détruisant pas le lecteur lorsque la permission de popup n'est pas accordée. diff --git a/fastlane/metadata/android/fr/changelogs/66.txt b/fastlane/metadata/android/fr/changelogs/66.txt new file mode 100644 index 000000000..3a94c81e0 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/66.txt @@ -0,0 +1,28 @@ +# Journal des modifications de la v0.13.6 + +### Améliorations + +- L'animation de l'icône du menu « hamburger » a été désactivée #1486 +- Annulation de la suppression des téléchargements #1472 +- Option de téléchargement dans le menu de partage #1498 +- Ajout d'une option de partage dans le menu "long tap" #1454 +- Réduction du lecteur principal à la sortie #1354 +- Mise à jour de la version de la bibliothèque et correction de la sauvegarde de la base de données #1510 +- Mise à jour de ExoPlayer 2.8.2 #1392 + - La boîte de dialogue de contrôle de la vitesse de lecture a été retravaillée pour prendre en charge différentes tailles de pas pour un changement de vitesse plus rapide. + - Ajout d'une option d'avance rapide pendant les silences dans le contrôle de la vitesse de lecture. Cela devrait être utile pour les livres audio et certains genres musicaux, et peut apporter une véritable expérience transparente (et peut casser une chanson avec beaucoup de silences =\\). + - Refonte de la résolution des sources de médias pour permettre le passage des métadonnées avec les médias en interne dans le lecteur, plutôt que de le faire manuellement. Maintenant, nous avons une seule source de métadonnées et elles sont directement disponibles lorsque la lecture commence. + - Correction des métadonnées des listes de lecture distantes qui ne sont pas mises à jour lorsque de nouvelles métadonnées sont disponibles lors de l'ouverture du fragment de liste de lecture. + - Diverses corrections de l'interface utilisateur : #1383, les contrôles de notification du lecteur en arrière-plan sont maintenant toujours blancs, il est plus facile de fermer le lecteur popup en le lançant. +- Utilisation d'un nouvel extracteur avec une architecture remaniée pour le multiservice. + +### Corrections + +- Correction #1440 Disposition des informations vidéo cassée #1491 +- Correction de l'historique des vues #1497 + - #1495, en mettant à jour les métadonnées (vignette, titre et nombre de vidéos) dès que l'utilisateur accède à la liste de lecture. + - #1475, en enregistrant une vue dans la base de données lorsque l'utilisateur lance une vidéo sur un lecteur externe sur le fragment de détail. +- Correction du timeout de la fenêtre en cas de mode popup. #1463 (Corrigé #640) +- Correction du lecteur vidéo principal #1509 + - Correction du mode répétition entraînant un NPE du lecteur lorsqu'une nouvelle intention est reçue alors que l'activité du lecteur est en arrière-plan. + - Correction de la réduction du lecteur en popup ne détruisant pas le lecteur lorsque la permission de popup n'est pas accordée. diff --git a/fastlane/metadata/android/fr/changelogs/68.txt b/fastlane/metadata/android/fr/changelogs/68.txt new file mode 100644 index 000000000..9b2760c35 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/68.txt @@ -0,0 +1,31 @@ +# Modifications v0.14.1 + +### Corrections +- Échec du décryptage de l'URL vidéo #1659 +- Lien de description, ne s'extrayait pas bien #1657 + +# Modifications v0.14.0 + +### Nouveautés +- Design du dossier #1461 +- Page d'accueil personnalisable #1461 + +### Améliorations +- Contrôles gestuels retravaillés #1604 +- Nouvelle façon de fermer le lecteur popup #1597 + +### Corrections +- Erreur lorsque le nombre d'abonnements n'est pas disponible. Ferme #1649. + - Affiche "le nombre d'abonnés non disponible" dans ces cas. +- NPE lorsqu'une liste de lecture YouTube est vide. +- Kiosques dans SoundCloud +- Refactor et correction du bug #1623 +- Résultat de recherche cyclique #1562 +- Barre de recherche qui n'est pas mise en page de manière statique +- Vidéos YT Premium qui ne sont pas bloquées correctement +- Vidéos qui ne se chargent pas toujours (à cause du parsing DASH) +- Liens dans la description des vidéos +- Afficher un avertissement lorsque quelqu'un essaie de télécharger vers une carte SD externe +- Exception "rien indiqué" qui déclenche un rapport +- La vignette ne s'affiche pas dans le lecteur de fond pour Android 8.1 [voir ici](https://github.com/TeamNewPipe/NewPipe/issues/943) +- Enregistrement du récepteur de diffusion. Ferme le dossier #1641. diff --git a/fastlane/metadata/android/fr/changelogs/69.txt b/fastlane/metadata/android/fr/changelogs/69.txt new file mode 100644 index 000000000..c96b390d9 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/69.txt @@ -0,0 +1,19 @@ +### Nouveau +- Suppression et partage par appui long dans les abonnements #1516 +- Interface utilisateur pour tablettes et disposition de la liste en grille #1617 + +### Améliorations +- Stockage/recharge du dernier rapport d'aspect utilisé #1748 +- Activation de la disposition linéaire dans l'activité Téléchargements avec les noms complets des vidéos #1771 +- Suppression et partage des abonnements directement à partir de l'onglet abonnements #1516 +- La mise en file d'attente déclenche désormais la lecture de la vidéo si la file d'attente de lecture est déjà terminée #1783 +- Paramètres distincts pour les gestes de volume et de luminosité #1644 +- Ajout de la prise en charge de la localisation #1792 + +### Corrections +- Analyse de l'heure pour le format . , afin que NewPipe puisse être utilisé en Finlande. +- Compte d'abonnement +- Ajout permission de service de premier plan pour les appareils API 28+ #1830 + +### Bugs connus +- État de lecture ne peut être enregistré sur Android P diff --git a/fastlane/metadata/android/fr/changelogs/70.txt b/fastlane/metadata/android/fr/changelogs/70.txt new file mode 100644 index 000000000..fccfccd2b --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/70.txt @@ -0,0 +1,25 @@ +ATTENTION : Cette version est probablement un festival de bugs, tout comme la dernière. Cependant, en raison de la fermeture complète depuis la 17. une version cassée est mieux que pas de version. N'est-ce pas ? ¯\_(ツ)_/¯ + +### Améliorations +* Les fichiers téléchargés peuvent maintenant être ouverts en un seul clic. +* Suppression du support pour Android 4.1 - 4.3 #1884 +* Suppression de l'ancien lecteur #1884 +* Suppression des flux de la file d'attente de lecture actuelle en les faisant glisser vers la droite #1915 +* Suppression du flux en file d'attente automatique lorsqu'un nouveau flux est mis en file d'attente manuellement #1878 +* Post-traitement pour les téléchargements et implémentation des fonctionnalités manquantes #1759 par @kapodamy + * Infrastructure de post-traitement + * Infrastructure de gestion des erreurs (pour le téléchargeur) + * File d'attente au lieu de téléchargements multiples + * Déplacer les téléchargements sérialisés en attente (fichiers `.giga`) vers les données de l'application. + * Implémentation de la répétition maximale des téléchargements + * Mise en pause des téléchargements multi-threads + * Arrêter les téléchargements lors du passage au réseau mobile (ne fonctionne jamais, voir 2ème point) + * Sauvegarder le nombre de threads pour les prochains téléchargements + * Beaucoup d'incohérences corrigées + +### Corrigé +* Correction d'un crash avec la résolution par défaut réglée sur la meilleure et la résolution limitée des données mobiles #1835 +* Correction du crash du lecteur de pop-up #1874 +* NPE lors de l'ouverture du lecteur de fond #1901 +* Correction de l'insertion de nouveaux flux lorsque la mise en file d'attente automatique est activée #1878 +* Correction du problème de décryptage de Shuttown diff --git a/fastlane/metadata/android/fr/changelogs/71.txt b/fastlane/metadata/android/fr/changelogs/71.txt index 0fa046111..4d1a5b1f6 100644 --- a/fastlane/metadata/android/fr/changelogs/71.txt +++ b/fastlane/metadata/android/fr/changelogs/71.txt @@ -1,10 +1,10 @@ ### Améliorations -* Notification maj GitHub (#1608 par @krtkush) -* Améliorations téléchargeur (#1944 par @kapodamy) : -  * icônes blanches manquantes ; utilisation d'une méthode pour changer leurs couleurs -  * vérification si l'itérateur est initialisé (#2031) - * réessayer les téléchargements post-processing failed dans le nouveau muxer -  * nouveau muxer MPEG-4 corrigeant les flux non synchrones (#2039) +* Notification maj GitHub #1608 +* Améliorations téléchargeur #1944 : +  * Ajout des icônes blanches manquantes et utilisation d'une méthode hardcodé pour changer leurs couleurs +  * Vérification si l'itérateur est initialisé (#2031) + * Autoriser le ré-essai de téléchargement après une erreur "post-processing failed" dans le nouveau muxer +  * Nouveau muxer MPEG-4 corrigeant les flux non synchrones (#2039) ### Corrections -* Flux YouTube en direct s'arrêtent (#1996 par @yausername) +* Flux YouTube en direct s'arrêtent #1996 diff --git a/fastlane/metadata/android/fr/changelogs/964.txt b/fastlane/metadata/android/fr/changelogs/964.txt new file mode 100644 index 000000000..4d65340fc --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/964.txt @@ -0,0 +1,8 @@ +• Ajout des chapitres dans lecteur +• [PeerTube] Ajout recherche en sépia +• Ajout bouton de partage en vue détaillée de la vidéo, déplacement description du flux dans l'onglet +• Désactivation restauration de luminosité si le geste est désactivé +• Ajout élément de liste pour lire vidéos sur Kodi +• Correction crash si aucun navigateur par défaut défini, amélioration dialogues de partage +• Basculer lecture/pause avec bouton d'espace matériel en lecteur plein écran +• [media.ccc.de] Corrections diff --git a/fastlane/metadata/android/fr/changelogs/966.txt b/fastlane/metadata/android/fr/changelogs/966.txt new file mode 100644 index 000000000..7cbe82fbf --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/966.txt @@ -0,0 +1,14 @@ +Nouveautés +• Ajout Bandcamp + +Améliorations +• Ajout option pour que application suive thème de l'appareil +• Prévention plantages par affichage panneau d'erreurs amélioré +• Plus d'informations sur raison indisponibilité contenu +• Bouton matériel espace déclenche lecture/pause +• Affichage toast "Téléchargement commencé" + +Corrections +• Très petite vignette dans détails de vidéo lors de lecture en arrière-plan +• Titre vide dans lecteur réduit +• Dernier mode redimensionnement pas restauré correctement diff --git a/fastlane/metadata/android/fr/changelogs/969.txt b/fastlane/metadata/android/fr/changelogs/969.txt new file mode 100644 index 000000000..6ac4a9467 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/969.txt @@ -0,0 +1,8 @@ +• Autoriser installation sur un stockage externe +• [Bandcamp] Ajout fonction permettant d'afficher les trois premiers commentaires d'un flux +• Afficher 'download has started' uniquement lorsque téléchargement lancé +• Ne pas définir cookie reCaptcha lorsqu'aucun n'est stocké +• [Player] Amélioration performances cache +• [Player] Correction problème lecture automatique +• Désactiver barres d'état précédentes lors suppr. des téléchargements +• Correction suppression objet ne figurant pas dans la liste diff --git a/fastlane/metadata/android/fr/changelogs/970.txt b/fastlane/metadata/android/fr/changelogs/970.txt new file mode 100644 index 000000000..928d37822 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/970.txt @@ -0,0 +1,11 @@ +Nouveautés +• Affichage métadonnées du contenu sous la description +• Ajout option "Afficher les détails de la chaîne" dans les playlists distantes +• Ajout option "Ouvrir dans le navigateur" dans le menu de la touche longue + +Corrections +• Correction d'un crash de rotation sur la page de détails de la vidéo +• Correction du bouton "Jouer avec Kodi" qui demande toujours d'installer Kore +• Correction chemins d'import/export des paramètres +• Correction nombre de commentaires aimés +Et bien plus encore diff --git a/fastlane/metadata/android/fr/changelogs/971.txt b/fastlane/metadata/android/fr/changelogs/971.txt new file mode 100644 index 000000000..3b302a06b --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/971.txt @@ -0,0 +1,3 @@ +Correctifs +• Augmentation de la mémoire tampon pour la lecture après le re-buffer +• Correction d'un crash sur les tablettes et les téléviseurs lors d'un clic sur l'icône de la file d'attente dans le lecteur diff --git a/fastlane/metadata/android/fr/changelogs/973.txt b/fastlane/metadata/android/fr/changelogs/973.txt new file mode 100644 index 000000000..667279399 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/973.txt @@ -0,0 +1,4 @@ +Correctifs +• Correction des vignettes et des titres qui sont coupés dans la mise en page en vue grille, dû à un calcul erroné du nombre de vidéos pouvant tenir dans une rangée. +• Correction de la boîte de dialogue de téléchargement qui disparaît sans rien faire si elle est ouverte à partir du menu de partage +• Maj d'une bibliothèque liée à l'ouverture d'activités externes telles que le sélecteur de fichiers du framewok d'accès stockage diff --git a/fastlane/metadata/android/fr/changelogs/974.txt b/fastlane/metadata/android/fr/changelogs/974.txt new file mode 100644 index 000000000..d963abe24 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/974.txt @@ -0,0 +1,5 @@ +Correctifs +• Correction des problèmes de mise en mémoire tampon causés par la restriction de débit de YouTube +• Correction de l'extraction des commentaires de YouTube et des plantages avec les commentaires désactivés +• Correction de la recherche de musique sur YouTube +• Correction des directs PeerTube diff --git a/fastlane/metadata/android/fr/changelogs/977.txt b/fastlane/metadata/android/fr/changelogs/977.txt new file mode 100644 index 000000000..6232ffa75 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/977.txt @@ -0,0 +1,8 @@ +• Ajout bouton "lecture suivante" au menu de la pression longue +• Ajout préfixe du chemin des shorts YouTube au filtre d'intention +• Correction importation des paramètres +• Permutation position barre de recherche avec boutons du lecteur dans l'écran de la file d'attente +• Corrections liées à MediasessionManager +• Correction barre de progression qui ne se termine pas après fin de vidéo +• Désactivation tunneling média sur RealtekATV +• Élargissement zone cliquable des boutons de lecture minimisés diff --git a/fastlane/metadata/android/fr/changelogs/980.txt b/fastlane/metadata/android/fr/changelogs/980.txt new file mode 100644 index 000000000..6835f70c8 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/980.txt @@ -0,0 +1,13 @@ +Nouveautés +• Ajout option "Ajouter à la liste de lecture" au menu de partage +• Ajout prise en charge des liens courts y2u.be et PeerTube + +Améliorations +• Commandes de vitesse de lecture plus compactes +• Le flux met désormais en évidence les nouveaux éléments +• L'option "Afficher les éléments surveillés" dans le flux est maintenant enregistrée + +Corrections +• Correction extraction des likes/dislikes de YouTube +• Correction relecture automatique après le retour de l'arrière-plan +Et bien d'autres diff --git a/fastlane/metadata/android/fr/changelogs/987.txt b/fastlane/metadata/android/fr/changelogs/987.txt index e0e1bd7bd..1641d9a00 100644 --- a/fastlane/metadata/android/fr/changelogs/987.txt +++ b/fastlane/metadata/android/fr/changelogs/987.txt @@ -1,8 +1,8 @@ Nouveautés -• Prise en charge de d'autres méthodes de diffusion que le HTTP progressif : temps de chargement plus rapide, corrections pour PeerTube et SoundCloud, lecture des nouveaux flux en directs de YouTube -• Boutton pour ajouter une liste de lecture distante à une locale +• Prise en charge d'autres méthodes de diffusion que le HTTP progressif : temps de chargement plus rapide, corrections pour PeerTube et SoundCloud, lecture des nouveaux flux en directs de YouTube +• Bouton pour ajouter une liste de lecture distante à une locale • Prévisualisation d'images lors d'un partage pour Andoid 10+ Améliorations • Amélioration de la boîte de dialogue des paramètres de la lecture -• Déplacement des bouttons importation/exportation vers le menu à trois points +• Déplacement des boutons importation/exportation vers le menu à trois points diff --git a/fastlane/metadata/android/fr/changelogs/988.txt b/fastlane/metadata/android/fr/changelogs/988.txt new file mode 100644 index 000000000..7ac03bb2a --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] Correction de l'erreur « Impossible d'obtenir un flux » lors de la lecture d'une vidéo +[YouTube] Correction du message « Le contenu suivant n'est pas disponible sur cette application. » affiché à la place de la vidéo demandée diff --git a/fastlane/metadata/android/fr/changelogs/989.txt b/fastlane/metadata/android/fr/changelogs/989.txt new file mode 100644 index 000000000..6d41c1b78 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/989.txt @@ -0,0 +1,3 @@ +• [YouTube] Correction du chargement infini lors de la lecture d'une vidéo +• [YouTube] Correction de l'accélération de certaines vidéos +• Mise à jour 1.15.3 de la bibliothèque jsoup contenant une correction de sécurité diff --git a/fastlane/metadata/android/fr/changelogs/990.txt b/fastlane/metadata/android/fr/changelogs/990.txt new file mode 100644 index 000000000..fcab79c3c --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/990.txt @@ -0,0 +1,15 @@ +Cette mise à jour abandonne la prise en charge d'Android 4.4 KitKat, la nouvelle version minimum est Android 5 Lollipop ! + +Nouveautés +• Télécharger depuis le menu d'appuis long +• Cacher les futures vidéos dans le flux +• Partager des listes de lecture locales + +Améliorations +• Réusinage du code du lecteur en petits composants : moins de mémoire vive utilisée, moins de bogues +• Meilleur redimension des miniatures +• Vectorisation des emplacements des images + +Corrections +• Notifications +• Plein écran diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt index a593ce32c..70048c15a 100644 --- a/fastlane/metadata/android/fr/short_description.txt +++ b/fastlane/metadata/android/fr/short_description.txt @@ -1 +1 @@ -Un lecteur multimédia libre et léger pour Android. +Une interface pour YouTube libre et légère sur Android. diff --git a/fastlane/metadata/android/he/changelogs/988.txt b/fastlane/metadata/android/he/changelogs/988.txt new file mode 100644 index 000000000..fc69457f7 --- /dev/null +++ b/fastlane/metadata/android/he/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] תוקנה השגיאה „אי אפשר לקבל שום תזרים” בעת ניסיון לנגן סרטונים +[YouTube] תוקנה ההודעה „התוכן הבא אינו זמין ביישומון הזה” שמופיעה במקום הסרטון המבוקש diff --git a/fastlane/metadata/android/hi/short_description.txt b/fastlane/metadata/android/hi/short_description.txt index 9e2aff567..3d76fa533 100644 --- a/fastlane/metadata/android/hi/short_description.txt +++ b/fastlane/metadata/android/hi/short_description.txt @@ -1 +1 @@ -एंड्रॉयड के लिए एक मुफ्त हल्का यूट्यूब फ्रंटएंड। +एंड्रॉयड के लिए एक मुफ्त लाइट यूट्यूब फ्रंटएंड। diff --git a/fastlane/metadata/android/hu/changelogs/65.txt b/fastlane/metadata/android/hu/changelogs/65.txt new file mode 100644 index 000000000..c3ee63ecc --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/65.txt @@ -0,0 +1,26 @@ +### Fejlesztések + +- A burgermenu ikon animációjának letiltása #1486 +- a letöltések törlésének visszavonása #1472 +- Letöltési lehetőség a #1498 megosztás menüben +- Megosztási lehetőség hozzáadva a hosszú érintéssel #1454 +- A fő játékos minimalizálása a 1354-es kijáratnál +- A könyvtár verziójának frissítése és az adatbázis biztonsági mentésének javítása #1510 +- ExoPlayer 2.8.2 frissítés #1392 + - Átdolgoztuk a lejátszási sebesség-vezérlő párbeszédpanelt, hogy támogassa a különböző lépésméreteket a gyorsabb sebességváltás érdekében. + - Hozzáadott egy kapcsolót a gyors előretekeréshez a lejátszási sebesség szabályozásában a csendek alatt. Ez hasznos lehet hangoskönyvek és bizonyos zenei műfajok esetében, és valódi zökkenőmentes élményt nyújthat (és megszakíthat egy dalt sok csenddel =\\). + - Átdolgozott médiaforrás felbontás, amely lehetővé teszi a metaadatok továbbítását a média mellett a lejátszón belül, nem pedig manuálisan. Most már egyetlen metaadatforrásunk van, és közvetlenül elérhető a lejátszás megkezdésekor. + - Javítva a távoli lejátszási lista metaadatai, amelyek nem frissülnek, amikor új metaadatok állnak rendelkezésre a lejátszási lista töredékének megnyitásakor. + - Különféle felhasználói felület-javítások: #1383, a háttérben lévő lejátszó értesítési vezérlői mostantól mindig fehérek, a felugró lejátszót egyszerűbben le lehet állítani dobással +- Használjon új kivonatot refaktorált architektúrával a többszolgáltatáshoz + +### Javítások + +- Javítás: #1440 Sérült videó információs elrendezés #1491 +- Előzmények megtekintése #1497. javítás + - #1495, a metaadatok (bélyegkép, cím és videószám) frissítésével, amint a felhasználó hozzáfér a lejátszási listához. + - #1475, egy nézet regisztrálásával az adatbázisban, amikor a felhasználó elindít egy videót a külső lejátszón a részletrészleten. +- Javítsa ki a képernyő időtúllépését felugró mód esetén. #1463 (fix #640) +- Fő videólejátszó javítás #1509 + - [#1412] Javítva az ismétlési mód, ami a játékos NPE-jét okozza, ha új szándék érkezik, miközben a játékos tevékenysége a háttérben van. + - Javítva, hogy a lejátszó előugró ablakra minimalizálja, nem semmisíti meg a lejátszót ha a popup engedélyt nem adják meg. diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt index 0a96f5e90..50752eeaf 100644 --- a/fastlane/metadata/android/hu/short_description.txt +++ b/fastlane/metadata/android/hu/short_description.txt @@ -1 +1 @@ -Egy ingyenes és könnyű YouTube előtétprogram Androidra. +Ingyenes, könnyű YouTube felület Androidra. diff --git a/fastlane/metadata/android/it/changelogs/63.txt b/fastlane/metadata/android/it/changelogs/63.txt index dd8f7324d..a342392ad 100644 --- a/fastlane/metadata/android/it/changelogs/63.txt +++ b/fastlane/metadata/android/it/changelogs/63.txt @@ -1,8 +1,8 @@ ### Miglioramenti -- Impostazioni di importazione / esportazione # 1333 -- Ridotto l'overdraw (miglioramento delle prestazioni) # 1371 -- Piccoli miglioramenti al codice # 1375 -- Aggiunto tutto ciò che riguarda il GDPR # 1420 +- Impostazioni di importazione / esportazione #1333 +- Ridotto l'overdraw (miglioramento delle prestazioni) #1371 +- Piccoli miglioramenti al codice #1375 +- Aggiunto tutto ciò che riguarda il GDPR #1420 ### Risolto -- Downloader: risolto il crash durante il caricamento di download incompleti dai file .giga # 1407 +- Downloader: risolto il crash durante il caricamento di download incompleti dai file .giga #1407 diff --git a/fastlane/metadata/android/it/changelogs/730.txt b/fastlane/metadata/android/it/changelogs/730.txt new file mode 100644 index 000000000..3df41556f --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/730.txt @@ -0,0 +1,2 @@ +# Risolto +- Sistemato di nuovo un errore nella funzione di decifrazione. diff --git a/fastlane/metadata/android/ko/changelogs/63.txt b/fastlane/metadata/android/ko/changelogs/63.txt new file mode 100644 index 000000000..69ca21e97 --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/63.txt @@ -0,0 +1,8 @@ +### 변경점 +- 불러오기/내보내기 세팅 #1333 +- 오버드로우 현상 개선 (성능 개선) #1371 +- 코드 일부분 개선 #1375 +- GDPR에 관한 모든것 업데이트 #1420 + +### 해결된것 +- 다운로더 : 다운로드가 완료되지 않은 .giga파일을 로딩할때 발생하는 에러 해결#1407 diff --git a/fastlane/metadata/android/ko/changelogs/64.txt b/fastlane/metadata/android/ko/changelogs/64.txt new file mode 100644 index 000000000..4cef85a41 --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/64.txt @@ -0,0 +1,8 @@ +### 변경점 +- 불러오기/내보내기 세팅 #1333 +- 오버드로우 현상 개선 (성능 개선) #1371 +- 코드 일부분 개선 #1375 +- GDPR에 관한 모든것 업데이트 #1420 + +### 고친것 +- 다운로더 : 다운로드가 완료되지 않은 .giga파일을 로딩할때 발생하는 문제 해결#1407 diff --git a/fastlane/metadata/android/pl/changelogs/964.txt b/fastlane/metadata/android/pl/changelogs/964.txt new file mode 100644 index 000000000..1684d6985 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/964.txt @@ -0,0 +1,8 @@ +• Dodano wsparcie rozdziałów w kontrolkach odtwarzacza +• [PeerTube] Dodano wyszukiwarkę Sepia +• Dodano ponownie przycisk udostępniania w sekcji szczegółów i przeniesiono informacje o strumieniu do układu karty +• Wyłączono przywracanie jasności przy wyłączonych gestach regulacji jasności +• Dodano element listy umożliwiający odtworzenie wideo w Kodi +• Naprawiono błąd przy braku domyślnej przeglądarki na niektórych urządzeniach i usprawniono menu udostępniania +• Przełączanie odtwarzanie/pauza poprzez wciśnięcie spacji na klawiaturze fizycznej w odtwarzaczu pełnoekranowym +• [media.cc.de] Różne poprawki i usprawnienia diff --git a/fastlane/metadata/android/pl/changelogs/965.txt b/fastlane/metadata/android/pl/changelogs/965.txt new file mode 100644 index 000000000..d336d984d --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/965.txt @@ -0,0 +1,5 @@ +Naprawiono błąd przy zmianie kolejności grup kanałów. +Naprawiono pobieranie kolejnych wideo z kanałów i playlist. +Naprawiono pobieranie komentarzy w YouTube. Dodano wsparcie dla ścieżek /watch/, /v/ oraz /w/ w URL-ach YouTube. +Naprawiono pobieranie ID użytkownika SoundCloud i zawartości z ograniczeniami geograficznymi. +Dodano język północnokurdyjski. diff --git a/fastlane/metadata/android/pl/changelogs/990.txt b/fastlane/metadata/android/pl/changelogs/990.txt new file mode 100644 index 000000000..4c292432b --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/990.txt @@ -0,0 +1,15 @@ +To wydanie znosi wsparcie dla Androida 4.4 KitKat, teraz min. wersja to Android 5 Lollipop! + +Nowe +• Pobier. z menu długiego naciśnięcia +• Ukryw. przyszłych wideo w kanale +• Udostęp. lokalnych playlist + +Ulepszone +• Refaktor. kodu odtwarzacza: mniejsze zużycie RAM-u, mniej błędów +• Skalowanie miniatur +• Wektoryzacja obrazków zastępczych + +Naprawione +• Różne problemy z powiadomieniem odtwarzacza: nieaktualne/brakujące info o multimediach, zniekształcona miniatura +• Pełny ekran zajmujący 1/4 ekranu diff --git a/fastlane/metadata/android/pt/changelogs/65.txt b/fastlane/metadata/android/pt/changelogs/65.txt new file mode 100644 index 000000000..89a006829 --- /dev/null +++ b/fastlane/metadata/android/pt/changelogs/65.txt @@ -0,0 +1,26 @@ +### Melhorias + +- Desativar a animação do ícone do burgermenu #1486 +- Desfazer a eliminação de descarregamentos #1472 +- Opção de descarregamento no menu de partilha #1498 +- Opção de partilha adicionada ao menu de toque longo #1454 +- Minimize o jogador principal na saída #1354 +- Atualização da versão da biblioteca e correção de cópia de segurança da base de dados #1510 +- ExoPlayer 2.8.2 Atualização #1392 +- Retrabalhado a caixa de diálogo de controlo de velocidade de reprodução para suportar diferentes tamanhos de etapa para uma mudança de velocidade mais rápida. +- Adicionado uma alternância para avanço rápido durante silêncios no controle de velocidade de reprodução. Isso deve ser útil para audiolivros e certos géneros musicais, e pode trazer uma experiência verdadeiramente perfeita (e pode quebrar uma música com muitos silêncios =\\). +- Resolução de fonte de média ré fatorada para permitir a passagem de metadados junto com a média internamente no reprodutor, em vez de fazê-lo manualmente. Agora temos uma única fonte de metadados e está disponível diretamente quando a reprodução é iniciada. +- Correção de metadados de listas de reprodução remotas que não são atualizadas quando novos metadados estão disponíveis quando o fragmento da lista de reprodução é aberta. +- Várias correções de interface do utilizador : #1383, controles de notificação do reprodutor em segundo plano agora sempre brancos, mais fácil de desligar o reprodutor pop-up por meio de arremesso +- Use novo extrator com arquitetura ré fatorada para multisserviço + +### Conserta + +- Correção #1440 Layout de informações de vídeo quebrado #1491 +-Ver correção de histórico #1497 +- #1495, atualizando os metadados (miniatura, título e contagem de vídeos) assim que o usuário acessar a lista de reprodução. +- #1475, registando uma visualização na base de dados quando o utilizador inicia um vídeo no reprodutor externo no fragmento de detalhes. +- Correção de tempo limite de criação em caso de modo pop-up. #1463 (Corrigido #640) +- Correção do reprodutor de vídeo principal #1509 +- [#1412] Corrigido o modo de repetição causando NPE do reprodutor quando uma nova intenção é recebida enquanto a atividade do reprodutor está em segundo plano. +- Corrigida a minimização de reprodutor para pop-up não destrói o reprodutor quando a permissão de pop-up não é concedida. diff --git a/fastlane/metadata/android/pt/changelogs/955.txt b/fastlane/metadata/android/pt/changelogs/955.txt index cd70b41c9..98bed58fe 100644 --- a/fastlane/metadata/android/pt/changelogs/955.txt +++ b/fastlane/metadata/android/pt/changelogs/955.txt @@ -1,3 +1,3 @@ -[YouTube] A procura por alguns utilizadores corrigida -[YouTube] Exceções de desencriptação aleatórias corrigidas -[SounCloud] URLs que terminam com uma barra são agora analisados corretamente +[YouTube] O problema com busca que afetava utilizadores foi corrigida +[YouTube] Exceções de desencriptação aleatórias foram corrigidas +[SounCloud] URLs que terminam com uma barra são analisadas corretamente diff --git a/fastlane/metadata/android/ru/changelogs/65.txt b/fastlane/metadata/android/ru/changelogs/65.txt new file mode 100644 index 000000000..51993fef3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/65.txt @@ -0,0 +1 @@ +эскиз видео diff --git a/fastlane/metadata/android/ru/changelogs/66.txt b/fastlane/metadata/android/ru/changelogs/66.txt new file mode 100644 index 000000000..51993fef3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/66.txt @@ -0,0 +1 @@ +эскиз видео diff --git a/fastlane/metadata/android/ru/changelogs/68.txt b/fastlane/metadata/android/ru/changelogs/68.txt new file mode 100644 index 000000000..51993fef3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/68.txt @@ -0,0 +1 @@ +эскиз видео diff --git a/fastlane/metadata/android/ru/changelogs/69.txt b/fastlane/metadata/android/ru/changelogs/69.txt new file mode 100644 index 000000000..c081690f8 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/69.txt @@ -0,0 +1 @@ +настройки diff --git a/fastlane/metadata/android/ru/changelogs/70.txt b/fastlane/metadata/android/ru/changelogs/70.txt new file mode 100644 index 000000000..da96c42fd --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/70.txt @@ -0,0 +1 @@ +всплывающий diff --git a/fastlane/metadata/android/ru/changelogs/780.txt b/fastlane/metadata/android/ru/changelogs/780.txt new file mode 100644 index 000000000..24cfa6e7f --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/780.txt @@ -0,0 +1,12 @@ +Изменения в 0.17.3 + +Улучшено +• Добавлена возможность очистки состояний воспроизведения #2550 +• Показ скрытых каталогов в средстве выбора файлов #2591 +• Поддержка URL-адресов из экземпляров `invidio.us`, открываемых с помощью NewPipe #2488 +• Добавлена поддержка URL-адресов `music.youtube.com` TeamNewPipe/NewPipeExtractor #194 + +Исправлено +• [YouTube] Исправлена ошибка java.lang.IllegalArgumentException #192 +• [YouTube] Исправлены неработающие прямые трансляции TeamNewPipe/NewPipeExtractor#195 +• Исправлена проблема с производительностью в Android Pie при загрузке потока #2592 diff --git a/fastlane/metadata/android/ru/changelogs/790.txt b/fastlane/metadata/android/ru/changelogs/790.txt new file mode 100644 index 000000000..24d1c115a --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/790.txt @@ -0,0 +1 @@ +папки diff --git a/fastlane/metadata/android/ru/changelogs/985.txt b/fastlane/metadata/android/ru/changelogs/985.txt new file mode 100644 index 000000000..d3978869d --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/985.txt @@ -0,0 +1 @@ +Исправлено: YouTube не воспроизводил никакие потоки diff --git a/fastlane/metadata/android/ru/changelogs/987.txt b/fastlane/metadata/android/ru/changelogs/987.txt new file mode 100644 index 000000000..8e9702365 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/987.txt @@ -0,0 +1,12 @@ +Новое +• Поддержка методов доставки, отличных от прогрессивного HTTP: ускорение времени загрузки воспроизведения, исправления PeerTube и SoundCloud, воспроизведение недавно закончившихся трансляций YouTube +• Кнопка «Добавить», чтобы добавить удаленный плейлист к локальному +• Предпросмотр изображения на странице общего доступа Android 10+ + +Улучшено +• Улучшения окна параметров воспроизведения +• Перемещение кнопки импорта/экспорта подписки в трехточечное меню + +Исправлено +• Исправлено удаление полностью просмотренных видео из плейлиста +• Исправлена тема меню «Поделиться» и пункт «Добавить в плейлист» diff --git a/fastlane/metadata/android/sk/changelogs/987.txt b/fastlane/metadata/android/sk/changelogs/987.txt new file mode 100644 index 000000000..45b2d85dd --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/987.txt @@ -0,0 +1,12 @@ +Novinky +• Poskytovanie iné než progresívne HTTP: zrýchlené načítanie prehrávania, opravy PeerTube a SoundCloud, prehrávanie nedávno ukončených livestreamov YouTube +• Tlačidlo na pridanie vzdialeného zoznamu k lokálnemu +• Náhľad obrázka v hárku zdieľania v Android 10+ + +Vylepšenia +• Dialógové okno parametrov prehrávania +• Presunuté tlačidlá import/export odberov do ○○○ menu + +Opravy +• Odstraňovanie dokončených videí zo zoznamu videí +• Téma v menu zdieľania a položky „pridať do zoznamu skladieb“ diff --git a/fastlane/metadata/android/sv/changelogs/65.txt b/fastlane/metadata/android/sv/changelogs/65.txt index 89c12c7d2..74d0a2d70 100644 --- a/fastlane/metadata/android/sv/changelogs/65.txt +++ b/fastlane/metadata/android/sv/changelogs/65.txt @@ -2,11 +2,23 @@ - Stängde av burgarmeny ikonens animation #1486 - Ångra radering av nedladdningar #1472 - Nedladdningsalternativ i delningsmenyn #1498 -- La till delningsalternativet i menyn för långa tryckningar #1454 -- Och mer... +- Lade till delningsalternativet i menyn för långa tryckningar #1454 +- Minimerar huvudsplearen vid avslut #1354 +- Uppdatering av biblioteksversion samt åtgärd av databasbackup #1510 +- Uppdatering av ExoPlayer 2.8.2 #1392 + - Omarbetad kontroll för uppspelningshastighet för att stödja olika stegstorlekar för snabbare hastighetsändring. + - Lade till växelkontroll för att snabbspola vid tystnad i uppspelningens hastighetskontroll. Detta borde vara underlätta vid uppspelning av ljudböcker och vissa musikgenres och kan bidra till en sömlös upplevelse ( och kan pajja en låt med massa tystnad =\\). + - Omskrivning av källmedias upplösning för att tillåta samtidig rörelse av metadata internt i spelaren, hellre än att utföra detta manuellt. Nu finns endast en källa för metadata som är omedelbart tillgängig så snart uppspelning sker. + - Åtgärdat att fjärrspellistors metadata inte uppdateras när nytt metadata är tillgänligt vid öppning av spelliststdelar. + - Diverse åtgärder av användargränssnitt: #1383, aviseringar för bakgrundsspelaren är nu alltid vita, lättare att stänga popup-spelare via "flinging" +- Nyttja ny extraherare med omskriven arkitektur för stöd av flera tjänster -### Fixade -- Fixade #1440 Trasig layout för videoinformation #1491 +### Åtgärdade +- Åtgärdade #1440 Trasig layout för videoinformation #1491 - Visningshistorik fix #1497 -- #1495, genom att uppdatera metadata (miniatyrbild, titel och videoantal) så snart användaren får tillgång till spellistan. - #1475 -- Och mer... +- #1495, genom att uppdatera metadata (miniatyrbild, titel och videoantal) så snart användaren får tillgång till spellistan. + - #1475, genom att skapa en vy i databasen när användaren startar en video i extern spelare för detaljfragment. +- Åtgärdade tidsgräns för fönster som är i popup-läge. #1463 (Fixed #640) +- Åtgärd av primär videospelare #1509 + - [#1412] Åtgärdade upprepningsläge vilket orsakade "null-pointer-exception" i spelaren när ny avsikt mottas för spelare som arbetar i bakgrunden. + - Åtgärdade att spelare utan popup-behörighet inte kraschar vid minimering till popupstorlek av fönstret. diff --git a/fastlane/metadata/android/sv/changelogs/969.txt b/fastlane/metadata/android/sv/changelogs/969.txt index a9ecf6b67..32725cc53 100644 --- a/fastlane/metadata/android/sv/changelogs/969.txt +++ b/fastlane/metadata/android/sv/changelogs/969.txt @@ -1,13 +1,8 @@ -• Tillåt installation på extern lagring - +• Tillåt installation på extern lagringsenhet • [Bandcamp] Stöd för att visa de tre första kommentarerna i en stream har lagts till. - • Visa endast "nedladdning har börjat" när nedladdningen har påbörjats. - • Ställ inte in reCaptcha-cookie när det inte finns någon cookie lagrad. - -• Player] Förbättra prestanda för cache - +• [Player] Förbättra prestanda för cache +• [Player] Åtgärdat ej automatisk uppspelning • Avskaffa tidigare Snackbars när nedladdningar raderas - -• Fixat att försöka radera objekt som inte finns i listan +• Åtgärdat försök att radera objekt som inte finns i listan diff --git a/fastlane/metadata/android/sv/changelogs/985.txt b/fastlane/metadata/android/sv/changelogs/985.txt index 4c434af54..35f298dbf 100644 --- a/fastlane/metadata/android/sv/changelogs/985.txt +++ b/fastlane/metadata/android/sv/changelogs/985.txt @@ -1 +1 @@ -Fixade att YouTube inte spelade någon stream. +Åtgärdat att YouTube inte spelar någon stream diff --git a/fastlane/metadata/android/tr/changelogs/63.txt b/fastlane/metadata/android/tr/changelogs/63.txt index b4ccdf68a..9370c537a 100644 --- a/fastlane/metadata/android/tr/changelogs/63.txt +++ b/fastlane/metadata/android/tr/changelogs/63.txt @@ -1,8 +1,8 @@ ### Geliştirmeler - İçe/Dışa aktarma ayarları #1333 -- Aşmalar azaltıldı(performance iyileştirmeleri) #1371 +- Aşmalar azaltıldı(performans iyileştirmeleri) #1371 - Küçük kod iyileştirmeleri #1375 - GPDR hakkında her şey eklendi #1420 ### Düzeltildi -- İndirici: .giga dosyalarından bitmeyen indirmeler yüklenirken çökmeler düzeltildi #1407 +- İndirici: .giga dosyalarından bitmemiş indirmeler yüklenirken çökmeler düzeltildi #1407 diff --git a/fastlane/metadata/android/tr/full_description.txt b/fastlane/metadata/android/tr/full_description.txt index e6ca6f1b4..11daef85b 100644 --- a/fastlane/metadata/android/tr/full_description.txt +++ b/fastlane/metadata/android/tr/full_description.txt @@ -1 +1,2 @@ -NewPipe herhangi bir Google çerçeve kütüphanesi veya YouTube API'si kullanmaz. Gereksindiği bilgileri edinirken yalnızca web sitesini ayrıştırır. Bu nedenle Google hizmetlerinin kurulmadığı aygıtlarda kullanılabilir. Ayrıca, NewPipe'ı kullanırken YouTube hesabına gereksinmezsiniz, özgür ve açık kaynaklı yazılımdır. +NewPipe herhangi bir Google çerçeve kütüphanesi veya YouTube API'ı kullanmaz. Sadece, ihtiyaç duyduğu bilgiyi edinmek için web sitesini ayrıştırır. +Bu nedenle Google hizmetlerinin kurulmadığı aygıtlarda kullanılabilir. Ayrıca, NewPipe'ı kullanırken YouTube hesabına ihtiyacınız yok, ve bu özgür ve açık kaynaklı bir yazılımdır. diff --git a/fastlane/metadata/android/uk/changelogs/988.txt b/fastlane/metadata/android/uk/changelogs/988.txt new file mode 100644 index 000000000..d882d430a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] Виправлено помилку «Не вдалося отримати жодного потоку» під час спроби відтворити будь-яке відео +[YouTube] Виправлено «Цей вміст недоступний у цьому застосунку.» замість запитаного відео diff --git a/fastlane/metadata/android/uk/changelogs/989.txt b/fastlane/metadata/android/uk/changelogs/989.txt new file mode 100644 index 000000000..2ccd1653a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/989.txt @@ -0,0 +1,3 @@ +• [YouTube] Виправлено нескінченне завантаження за спроби відтворити будь-яке відео +• [YouTube] Виправлено тротлінг на деяких відео +• Оновлено бібліотеку jsoup до версії 1.15.3, яка включає виправлення безпеки diff --git a/fastlane/metadata/android/uk/changelogs/990.txt b/fastlane/metadata/android/uk/changelogs/990.txt new file mode 100644 index 000000000..bc1d0a12c --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/990.txt @@ -0,0 +1,15 @@ +Припинено підтримку Android 4.4 KitKat, тепер найнижча версія — Android 5 Lollipop! + +Нове +• Завантаження з меню при затисненні +• Ховання майбутніх відео в стрічці +• Поширення локальних добірок + +Поліпшено +• Код поділено на компоненти: менше використання пам'яті, менше вад +• Удосконалено режим масштабування мініатюр +• Замінено картинки-заглушки на векторні + +Виправлено +• Сповіщення: застарілі/відсутні дані про медіафайл, викривлену мініатюру +• Використання повноекранним режимом лише чверті екрана diff --git a/fastlane/metadata/android/zh-Hans/changelogs/988.txt b/fastlane/metadata/android/zh-Hans/changelogs/988.txt new file mode 100644 index 000000000..67f53fc8f --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] 修复 试图播放任何视频时,显示"无法获得任何流 " +[YouTube] 修复 显示"以下内容在此应用中不可用 ",而不是所需要的视频 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/989.txt b/fastlane/metadata/android/zh-Hans/changelogs/989.txt new file mode 100644 index 000000000..0c89cb986 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/989.txt @@ -0,0 +1,3 @@ +- [YouTube] 修复 尝试播放任何视频时无限加载 +- [YouTube] 修复 某些视频的节流问题 +- 将jsoup库升级到1.15.3,其中包括一个安全修复 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/990.txt b/fastlane/metadata/android/zh-Hans/changelogs/990.txt new file mode 100644 index 000000000..2b3886a0a --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/990.txt @@ -0,0 +1,15 @@ +此版本移除了对 Android 4.4 KitKat 的支持,现在支持的最低版本是 Android 5 Lollipop! + +新增 +• 在长按菜单中进行下载 +• 隐藏 Feed 中的未来视频 +• 分享本地播放列表 + +改进 +• 重构播放器代码成多个小组件:使用的内存更少了,BUG 更少了 +• 改进了缩略图的缩放方式 +• 矢量化了占位图片 + +修复 +• 修复播放器通知相关的几个问题:媒体信息过期或缺失,缩略图扭曲变形 +• 修复全屏模式仅使用了 1/4 屏幕 diff --git a/fastlane/metadata/android/zh-Hant/changelogs/988.txt b/fastlane/metadata/android/zh-Hant/changelogs/988.txt new file mode 100644 index 000000000..684c88444 --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] 修正嘗試播放任何影片時「無法取得任何串流」的錯誤 +[YouTube] 修正請求影片時顯示「以下內容不在此應用程式中可用」的訊息 diff --git a/fastlane/metadata/android/zh-Hant/changelogs/989.txt b/fastlane/metadata/android/zh-Hant/changelogs/989.txt new file mode 100644 index 000000000..fdf05181a --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/changelogs/989.txt @@ -0,0 +1,3 @@ +• [YouTube] 修正嘗試播放任何影片時無盡載入 +• [YouTube] 修正部分影片限速 +• 升級 jsoup 程式庫至 1.15.3,當中包含一項安全修正 diff --git a/fastlane/metadata/android/zh-Hant/changelogs/990.txt b/fastlane/metadata/android/zh-Hant/changelogs/990.txt new file mode 100644 index 000000000..3260f395e --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/changelogs/990.txt @@ -0,0 +1,15 @@ +是次發布終止支援 Android 4.4 KitKat,最低版本現為 Android 5 Lollipop! + +新增 +• 長按功能表中的下載選項 +• 摘要隱藏未到時候的影片 +• 分享本機播放清單 + +改進 +• 重構播放器程式碼為若干小元件:佔用較少 RAM,出現較少錯誤 +• 改進縮圖的縮放模式 +• 向量化預留位置影像 + +修正 +• 修正播放器通知的若干問題:媒體資訊過時/欠奉、縮圖失真 +• 修正全螢幕僅佔用 1/4 畫面 diff --git a/fastlane/metadata/android/zh_Hant_HK/changelogs/988.txt b/fastlane/metadata/android/zh_Hant_HK/changelogs/988.txt new file mode 100644 index 000000000..bcc006d34 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant_HK/changelogs/988.txt @@ -0,0 +1,2 @@ +[YouTube] 修正播咩片都「攞唔到任何串流」嘅問題 +[YouTube] 修正出現「呢部內容喺呢個 app 欠奉」嘅訊息,睇唔到請求嘅影片 diff --git a/fastlane/metadata/android/zh_Hant_HK/changelogs/989.txt b/fastlane/metadata/android/zh_Hant_HK/changelogs/989.txt new file mode 100644 index 000000000..296c2ada4 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant_HK/changelogs/989.txt @@ -0,0 +1,3 @@ +• [YouTube] 執返好播咩片都係噉轉 lo 唔到 +• [YouTube] 執返好有啲片窒下窒下 +• 將 jsoup 程式庫升級做 1.15.3,包括修正一個保安問題 diff --git a/fastlane/metadata/android/zh_Hant_HK/changelogs/990.txt b/fastlane/metadata/android/zh_Hant_HK/changelogs/990.txt new file mode 100644 index 000000000..a91ea0e67 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant_HK/changelogs/990.txt @@ -0,0 +1,15 @@ +今次版本要扔低 Android 4.4 KitKat 啦,而家起最起碼要 Android 5 Lollipop 至裝到呢個 app! + +新嘢 +• 撳實有得揀下載 +• 摘要飛起未夠鐘上畫嘅片 +• 分享本機嘅播放清單 + +進步 +• 翻新播放器程式碼劏做幾部細件:用少啲 RAM、冇咁多 bug +• 錶起啲縮圖嘅時候擺得靚仔啲 +• 啲楔位公仔轉做向量圖 + +執漏 +• 修正播放器通知嘅問題:多媒體資訊過時/留空、縮圖鬆郁矇 +• 修正全螢幕用得 1/4 個螢幕 diff --git a/gradle.properties b/gradle.properties index 76b51ef0b..032d70cee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -android.enableJetifier=true +android.enableJetifier=false android.useAndroidX=true org.gradle.jvmargs=-Xmx2048M systemProp.file.encoding=utf-8 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4..249e5832f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4ed3bdea4..5116c5b18 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip -distributionSha256Sum=e6d864e3b5bc05cc62041842b306383fc1fefcec359e70cebb1d470a6094ca82 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c78733..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal
BitcoinBitcoin QR code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
Liberapay Visit NewPipe at liberapay.com Donate via Liberapay
BitcoinBitcoin QR code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
Bountysource Visit NewPipe at bountysource.com