diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 088707b6ee..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,88 +0,0 @@ -### Please do **not** open pull requests for *new features* now, as we are planning to rewrite large chunks of the code. Only bugfix PRs will be accepted. More details will be announced soon! - -NewPipe contribution guidelines -=============================== - -## Crash reporting - -Report crashes through the **automated crash report system** of NewPipe. -This way all the data needed for debugging is included in your bugreport for GitHub. -You'll see *exactly* what is sent, be able to add **your comments**, and then send it. - -## Issue reporting/feature requests - -* **Already reported**? Browse the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) to make sure your issue/feature hasn't been reported/requested. -* **Already fixed**? Check whether your issue/feature is already fixed/implemented. -* **Still relevant**? Check if the issue still exists in the latest release/beta version. -* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! See [Code contribution](#code-contribution) for more info. -* **Is it in English**? Issues in other languages will be ignored unless someone translates them. -* **Is it one issue**? Multiple issues require multiple reports, that can be linked to track their statuses. -* **The template**: Fill it out, everyone wins. Your issue has a chance of getting fixed. - - -## Translation - -* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register. -* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki. -* NewPipe uses the [PrettyTime](https://github.com/ocpsoft/prettytime) library to display localized versions of dates and times. It needs to be translated, too. Read [these instructions to add a new language](https://www.ocpsoft.org/prettytime/#section-14) and [this issue](https://github.com/TeamNewPipe/NewPipe/issues/9134) for more info. - -## Code contribution - -### Guidelines - -* Stick to NewPipe's *style conventions* of [checkStyle](https://github.com/checkstyle/checkstyle) and [ktlint](https://github.com/pinterest/ktlint). They run each time you build the project. -* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy). -* In particular **do not bring non-free software** (e.g. binary blobs) into the project. Make sure you do not introduce any closed-source library from Google. - -### Before starting development - -* If you want to help out with an existing bug report or feature request, **leave a comment** on that issue saying you want to try your hand at it. -* If there is no existing issue for what you want to work on, **open a new one** describing the changes you are planning to introduce. This gives the team and the community a chance to give **feedback** before you spend time on something that is already in development, should be done differently, or should be avoided completely. -* Please show **intention to maintain your features** and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description. -* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions. -* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out. - -### Kotlin in NewPipe -* NewPipe will remain mostly Java for time being -* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language. - -### Creating a Pull Request (PR) - -* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub. -* Please **test** (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged! -* Respond if someone requests changes or otherwise raises issues about your PRs. -* Try to figure out yourself why builds on our CI fail. -* Make sure your PR is **up-to-date** with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must *rebase* your branch on the `dev` branch manually and resolve the conflicts on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). Doing this makes the maintainers' job way easier. - -## IDE setup & building the app - -### Basic setup - -NewPipe is developed using [Android Studio](https://developer.android.com/studio/). Learn more about how to install it and how it works in the [official documentation](https://developer.android.com/studio/intro). In particular, make sure you have accepted Android Studio's SDK licences. Once Android Studio is ready, setting up the NewPipe project is fairly simple: -- Clone the NewPipe repository with `git clone https://github.com/TeamNewPipe/NewPipe.git` (or use the link from your own fork, if you want to open a PR). -- Open the folder you just cloned with Android Studio. -- Build and run it just like you would do with any other app, with the green triangle in the top bar. - -You may find [SonarLint](https://www.sonarlint.org/intellij)'s **inspections** useful in helping you to write good code and prevent bugs. - -### checkStyle setup - -The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that Java code abides by the project style. It runs automatically each time you build the project. If you want to view errors directly in the editor, instead of having to skim through the build output, you can install an Android Studio plugin: -- Go to `File -> Settings -> Plugins`, search for `checkstyle` and install `CheckStyle-IDEA`. -- Go to `File -> Settings -> Tools -> Checkstyle`. -- Add NewPipe's configuration file by clicking the `+` in the right toolbar of the "Configuration File" list. -- Under the "Use a local Checkstyle file" bullet, click on `Browse` and, enter `checkstyle` folder under the project's root path and pick the file named `checkstyle.xml`. -- Enable "Store relative to project location" so that moving the directory around does not create issues. -- Insert a description in the top bar, then click `Next` and then `Finish`. -- Activate the configuration file you just added by enabling the checkbox on the left. -- Click `Ok` and you are done. - -### ktlint setup - -The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as checkStyle for Kotlin files. Installing the related plugin is as simple as going to `File -> Settings -> Plugins`, searching for `ktlint` and installing `Ktlint (unofficial)`. - -## Communication - -* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)! -* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link. -* You can post your suggestions, changes, ideas etc. on either GitHub or IRC. diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml deleted file mode 100644 index 2d467d5e5a..0000000000 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ /dev/null @@ -1,34 +0,0 @@ -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this form! :hugs: - - Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe). - - - type: checkboxes - id: checklist - attributes: - label: "Checklist" - options: - - label: "I made sure that there are *no existing issues or discussions* - [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)." - required: true - - - type: textarea - id: what-is-the-question - attributes: - label: What is/are your question(s)? - validations: - required: true - - - type: textarea - id: additional-information - attributes: - label: Additional information - description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4137c7635d..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -liberapay: TeamNewPipe -custom: 'https://newpipe.net/donate/' diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 52897f1acb..ebfabd29fc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,22 +1,17 @@ name: Bug report description: Create a bug report to help us improve -labels: [bug, needs triage] +labels: [bug] body: - - type: markdown - attributes: - value: | - Thank you for helping to make NewPipe better by reporting a bug. :hugs: - - Please fill in as much information as possible about your bug so that we don't have to play "information ping-pong" and can help you immediately. - - type: checkboxes id: checklist attributes: label: "Checklist" options: - - label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)." + - label: "I am able to reproduce the bug with the [latest version](https://github.com/polymorphicshade/Tubular/releases/latest)." + required: true + - label: "I am *not* able to reproduce the bug with the *latest version* of [vanilla NewPipe](https://github.com/TeamNewPipe/NewPipe/releases/)." 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." + - label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/Tubular/issues) or [closed](https://github.com/polymorphicshade/Tubular/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 @@ -24,14 +19,14 @@ body: required: true - label: "This issue contains only one bug." required: true - - label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." + - label: "I have read and understood the vanilla NewPipe [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." required: true - type: input id: app-version attributes: label: Affected version - description: "In which NewPipe version did you encounter the bug?" + description: "In which version did you encounter the bug?" placeholder: "x.xx.x - Can be seen in the app from the 'About' section in the sidebar" validations: required: true @@ -68,7 +63,7 @@ body: - type: textarea id: screen-media attributes: - label: Screenshots/Screen recordings + label: Screenshots/Recordings description: | A picture or video is worth a thousand words. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 4721637bf6..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: ❓ Question - url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions - about: Ask about anything NewPipe-related - - name: 💬 IRC - url: https://web.libera.chat/#newpipe - about: Chat with us via IRC for quick Q/A - - name: 💬 Matrix - url: https://matrix.to/#/#newpipe:libera.chat - about: Chat with us via Matrix for quick Q/A diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 31ef92c44f..6a3e810f54 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,29 +1,23 @@ name: Feature request description: Suggest an idea for this project -labels: [feature request, needs triage] +labels: [] body: - - type: markdown - attributes: - value: | - 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: label: "Checklist" 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." + - label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/Tubular/issues) or [closed](https://github.com/polymorphicshade/Tubular/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)." + - label: "I'm aware that this is a request for Tubular itself and that requests for adding a new service need to be made in [vanilla 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." required: true - label: "This issue contains only one feature request." required: true - - label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." + - label: "I have read and understood the vanilla NewPipe [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." required: true @@ -45,6 +39,17 @@ body: validations: required: true + - type: textarea + id: why-relevant-to-fork + attributes: + label: Why ist the feature relevant to this fork? + description: | + Describe why your feature is relevant to the features this fork aims to provide over vanilla NewPipe, or demonstrate that [upstream](https://github.com/TeamNewPipe/NewPipe/issues) is *explicitly* against implementing your feature for *non-technical reasons*. + + (This is not the place for stale upstream feature requests!) + validations: + required: true + - type: textarea id: additional-information attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 407c00a39b..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,34 +0,0 @@ - - -#### What is it? -- [ ] Bugfix (user facing) -- [ ] Feature (user facing) -- [ ] Codebase improvement (dev facing) -- [ ] Meta improvement to the project (dev facing) - -#### Description of the changes in your PR - -- record videos -- create clones -- take over the world - -#### Before/After Screenshots/Screen Record - -- Before: -- After: - -#### Fixes the following issue(s) - -- Fixes # - -#### Relies on the following changes - -- - -#### 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. You can find more info and a video demonstration [on this wiki page](https://github.com/TeamNewPipe/NewPipe/wiki/Download-APK-for-PR). - -#### Due diligence -- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md). diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml deleted file mode 100644 index 902f376c00..0000000000 --- a/.github/changed-lines-count-labeler.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Add 'size/small' label to any changes with less than 50 lines -size/small: - max: 49 - -# Add 'size/medium' label to any changes between 50 and 249 lines -size/medium: - min: 50 - max: 249 - -# Add 'size/large' label to any changes between 250 and 749 lines -size/large: - min: 250 - max: 749 - -# Add 'size/giant' label to any changes for more than 749 lines -size/giant: - min: 750 diff --git a/.github/changelog.md b/.github/changelog.md new file mode 100644 index 0000000000..b73fdf6bd1 --- /dev/null +++ b/.github/changelog.md @@ -0,0 +1,14 @@ +New +- "Shuffle and play" menu option for playlists +- Option to show dislikes as a percentage + +Improved +- Streamlined the release process with Github Actions (thanks @thekyber) + +Fixed +- The database should now be exportable to vanilla NewPipe (this breaks the SponsorBlock whitelist functionality and will be fixed in a later version) +- Changed some graphics to be specific to Tubular +- Privacy-related changes to comply with FDroid rules +- No longer crashing when attempting to send a crash report with a huge stack trace +- Unskip didn't work on certain segments sometimes (thanks @mikooomich) +- Some translation changes (thanks @onkq, @mikropsoft, @Mr-Bajs) \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d76e16456..0ac5e7cb62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: branches: - dev - master - - release** paths-ignore: - 'README.md' - 'doc/**' @@ -29,24 +28,19 @@ on: - '.github/ISSUE_TEMPLATE/**' jobs: - build-and-test-jvm: + build: runs-on: ubuntu-latest - permissions: - contents: read + permissions: write-all steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - name: Checkout branch "${{ github.ref_name }}" + run: | + git clone --no-checkout https://github.com/polymorphicshade/Tubular.git . + git config core.symlinks false + git checkout --progress --force ${{ github.ref_name }} - - name: create and checkout branch - # push events already checked out the branch - if: github.event_name == 'pull_request' - env: - BRANCH: ${{ github.head_ref }} - run: git checkout -B "$BRANCH" - - - name: set up JDK 17 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: 17 @@ -61,76 +55,3 @@ jobs: with: name: app path: app/build/outputs/apk/debug/*.apk - - test-android: - # macos has hardware acceleration. See android-emulator-runner action - runs-on: macos-latest - timeout-minutes: 20 - strategy: - matrix: - include: - - api-level: 21 - target: default - arch: x86 - - api-level: 33 - target: google_apis # emulator API 33 only exists with Google APIs - arch: x86_64 - - permissions: - contents: read - - steps: - - uses: actions/checkout@v4 - - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: "temurin" - cache: 'gradle' - - - name: Run android tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: ${{ matrix.arch }} - 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@v4 - if: failure() - with: - name: android-test-report-api${{ matrix.api-level }} - path: app/build/reports/androidTests/connected/** - - sonar: - runs-on: ubuntu-latest - - permissions: - contents: read - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: "temurin" - cache: 'gradle' - - - name: Cache SonarCloud packages - uses: actions/cache@v4 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonar --info diff --git a/.github/workflows/image-minimizer.js b/.github/workflows/image-minimizer.js deleted file mode 100644 index d099068ba8..0000000000 --- a/.github/workflows/image-minimizer.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Script for minimizing big images (jpg,gif,png) when they are uploaded to GitHub and not edited otherwise - */ -module.exports = async ({github, context}) => { - const IGNORE_KEY = ''; - const IGNORE_ALT_NAME_END = 'ignoreImageMinify'; - // Targeted maximum height - const IMG_MAX_HEIGHT_PX = 600; - // maximum width of GitHub issues/comments - const IMG_MAX_WIDTH_PX = 800; - // all images that have a lower aspect ratio (-> have a smaller width) than this will be minimized - const MIN_ASPECT_RATIO = IMG_MAX_WIDTH_PX / IMG_MAX_HEIGHT_PX - - // Get the body of the image - let initialBody = null; - if (context.eventName == 'issue_comment') { - initialBody = context.payload.comment.body; - } else if (context.eventName == 'issues') { - initialBody = context.payload.issue.body; - } else if (context.eventName == 'pull_request') { - initialBody = context.payload.pull_request.body; - } else { - console.log('Aborting: No body found'); - return; - } - console.log(`Found body: \n${initialBody}\n`); - - // Check if we should ignore the currently processing element - if (initialBody.includes(IGNORE_KEY)) { - console.log('Ignoring: Body contains IGNORE_KEY'); - return; - } - - // Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com//.) - const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm; - const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm; - - // Check if we found something - let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody) - || REGEX_ASSETS_IMAGE_LOCKUP.test(initialBody); - if (!foundSimpleImages) { - console.log('Found no simple images to process'); - return; - } - - console.log('Found at least one simple image to process'); - - // Require the probe lib for getting the image dimensions - const probe = require('probe-image-size'); - - var wasMatchModified = false; - - // Try to find and replace the images with minimized ones - let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync); - newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOCKUP, minimizeAsync); - - if (!wasMatchModified) { - console.log('Nothing was modified. Skipping update'); - return; - } - - // Update the corresponding element - if (context.eventName == 'issue_comment') { - console.log('Updating comment with id', context.payload.comment.id); - await github.rest.issues.updateComment({ - comment_id: context.payload.comment.id, - owner: context.repo.owner, - repo: context.repo.repo, - body: newBody - }) - } else if (context.eventName == 'issues') { - console.log('Updating issue', context.payload.issue.number); - await github.rest.issues.update({ - issue_number: context.payload.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: newBody - }); - } else if (context.eventName == 'pull_request') { - console.log('Updating pull request', context.payload.pull_request.number); - await github.rest.pulls.update({ - pull_number: context.payload.pull_request.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: newBody - }); - } - - // Async replace function from https://stackoverflow.com/a/48032528 - async function replaceAsync(str, regex, asyncFn) { - const promises = []; - str.replace(regex, (match, ...args) => { - const promise = asyncFn(match, ...args); - promises.push(promise); - }); - const data = await Promise.all(promises); - return str.replace(regex, () => data.shift()); - } - - async function minimizeAsync(match, g1, g2) { - console.log(`Found match '${match}'`); - - if (g1.endsWith(IGNORE_ALT_NAME_END)) { - console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`); - return match; - } - - let probeAspectRatio = 0; - let shouldModify = false; - try { - console.log(`Probing ${g2}`); - let probeResult = await probe(g2); - if (probeResult == null) { - throw 'No probeResult'; - } - if (probeResult.hUnits != 'px') { - throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`; - } - if (probeResult.height <= 0) { - throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`; - } - if (probeResult.wUnits != 'px') { - throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`; - } - if (probeResult.width <= 0) { - throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`; - } - console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`); - - probeAspectRatio = probeResult.width / probeResult.height; - shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && probeAspectRatio < MIN_ASPECT_RATIO; - } catch(e) { - console.log('Probing failed:', e); - // Immediately abort - return match; - } - - if (shouldModify) { - wasMatchModified = true; - console.log(`Modifying match '${match}'`); - return `${g1}`; - } - - console.log(`Match '${match}' is ok/will not be modified`); - return match; - } -} diff --git a/.github/workflows/image-minimizer.yml b/.github/workflows/image-minimizer.yml deleted file mode 100644 index d9241c33b6..0000000000 --- a/.github/workflows/image-minimizer.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Image Minimizer - -on: - issue_comment: - types: [created, edited] - issues: - types: [opened, edited] - pull_request: - types: [opened, edited] - -permissions: - issues: write - pull-requests: write - -jobs: - try-minimize: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 16 - - - name: Install probe-image-size - run: npm i probe-image-size@7.2.3 --ignore-scripts - - - name: Minimize simple images - uses: actions/github-script@v7 - timeout-minutes: 3 - with: - script: | - const script = require('.github/workflows/image-minimizer.js'); - await script({github, context}); diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml deleted file mode 100644 index b3495135ff..0000000000 --- a/.github/workflows/no-response.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: No Response - -# Both `issue_comment` and `scheduled` event types are required for this Action -# to work properly. -on: - issue_comment: - types: [created] - schedule: - # Run daily at midnight. - - cron: '0 0 * * *' - -permissions: - issues: write - pull-requests: write - -jobs: - noResponse: - runs-on: ubuntu-latest - steps: - - uses: lee-dohm/no-response@v0.5.0 - with: - token: ${{ github.token }} - daysUntilClose: 14 - responseRequiredLabel: waiting for author diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml deleted file mode 100644 index a18daca3ad..0000000000 --- a/.github/workflows/pr-labeler.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: "PR size labeler" -on: [pull_request_target] -permissions: - contents: read - pull-requests: write - -jobs: - changed-lines-count-labeler: - runs-on: ubuntu-latest - name: Automatically labelling pull requests based on the changed lines count - permissions: - pull-requests: write - steps: - - name: Set a label - uses: TeamNewPipe/changed-lines-count-labeler@main - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/changed-lines-count-labeler.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..6dda6a9a2c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release + +on: + workflow_dispatch: + inputs: + title: + type: string + description: 'Title' + required: true + default: 'v0.00.0 R0' + is_pre_release: + type: boolean + description: 'Set as a pre-release' + required: true + default: true + +jobs: + build-and-release: + runs-on: ubuntu-latest + + permissions: write-all + + steps: + - name: Checkout branch "${{ github.ref_name }}" + run: | + git clone --no-checkout https://github.com/polymorphicshade/Tubular.git . + git config core.symlinks false + git checkout --progress --force ${{ github.ref_name }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: "temurin" + cache: 'gradle' + + - name: Build release APK + run: ./gradlew assembleRelease + + - name: Sign APK + env: + KEYSTORE: ${{ secrets.KEYSTORE }} + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} + run: | + version=$( grep "versionName" app/build.gradle | awk -F'"' '{print $2}' ) + echo "${KEYSTORE}" | base64 -d > apksign.keystore + ${ANDROID_HOME}/build-tools/34.0.0/apksigner sign --ks apksign.keystore --ks-pass env:SIGNING_STORE_PASSWORD "app/build/outputs/apk/release/app-release-unsigned.apk" + mv app/build/outputs/apk/release/app-release-unsigned.apk app/build/outputs/apk/release/"tubular_v${version}.apk" + + - name: Create release and upload + run: | + version=$( grep "versionName" app/build.gradle | awk -F'"' '{print $2}' ) + gh auth login --with-token <<<"${{ secrets.GITHUB_TOKEN }}" + gh release create "v${version}" --title "${{ inputs.title }}" --notes-file ".github/changelog.md" --prerelease=${{ inputs.is_pre_release }} --repo polymorphicshade/Tubular + gh release upload "v${version}" app/build/outputs/apk/release/*.apk --repo polymorphicshade/Tubular + + - name: Archive reports for job + uses: actions/upload-artifact@v4 + with: + name: reports + path: '*/build/reports' + if: ${{ always() }} diff --git a/app/build.gradle b/app/build.gradle index 397a7301e9..c67ec7ce95 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { namespace 'org.schabi.newpipe' defaultConfig { - applicationId "org.schabi.newpipe" - resValue "string", "app_name", "NewPipe" + applicationId "org.polymorphicshade.tubular" + resValue "string", "app_name", "Tubular" minSdk 21 targetSdk 33 versionCode 998 @@ -42,19 +42,19 @@ android { if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") { // default values when branch name could not be determined or is master or dev applicationIdSuffix ".debug" - resValue "string", "app_name", "NewPipe Debug" + resValue "string", "app_name", "Tubular Debug" } else { applicationIdSuffix ".debug." + normalizedWorkingBranch - resValue "string", "app_name", "NewPipe " + workingBranch - archivesBaseName = 'NewPipe_' + normalizedWorkingBranch + resValue "string", "app_name", "Tubular " + workingBranch + archivesBaseName = 'Tubular_' + normalizedWorkingBranch } } release { if (System.properties.containsKey('packageSuffix')) { applicationIdSuffix System.getProperty('packageSuffix') - resValue "string", "app_name", "NewPipe " + System.getProperty('packageSuffix') - archivesBaseName = 'NewPipe_' + System.getProperty('packageSuffix') + resValue "string", "app_name", "Tubular " + System.getProperty('packageSuffix') + archivesBaseName = 'Tubular_' + System.getProperty('packageSuffix') } minifyEnabled true shrinkResources false // disabled to fix F-Droid's reproducible build @@ -103,6 +103,13 @@ android { 'META-INF/COPYRIGHT'] } } + + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } } ext { @@ -198,7 +205,7 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.1' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.0' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json new file mode 100644 index 0000000000..58d6c275a1 --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json @@ -0,0 +1,730 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "7591e8039faa74d8c0517dc867af9d3e", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationMode", + "columnName": "notification_mode", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploaderUrl", + "columnName": "uploader_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isUploadDateApproximation", + "columnName": "is_upload_date_approximation", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "access_date" + ] + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressMillis", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isThumbnailPermanent", + "columnName": "is_thumbnail_permanent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailStreamId", + "columnName": "thumbnail_stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "playlist_id", + "join_index" + ] + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_feed_group_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed_group_subscription_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "feedGroupId", + "columnName": "group_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "group_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "feed_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "group_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_last_updated", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "subscription_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/7.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/7.json index a14f8b9a8f..510eb497ea 100644 --- a/app/schemas/org.schabi.newpipe.database.AppDatabase/7.json +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/7.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 7, - "identityHash": "012fc8e7ad3333f1597347f34e76a513", + "identityHash": "7dcdec7a500be9088f7a9a4767292b41", "entities": [ { "tableName": "subscriptions", @@ -58,10 +58,10 @@ } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "uid" - ], - "autoGenerate": true + ] }, "indices": [ { @@ -107,10 +107,10 @@ } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "id" - ], - "autoGenerate": true + ] }, "indices": [ { @@ -209,10 +209,10 @@ } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "uid" - ], - "autoGenerate": true + ] }, "indices": [ { @@ -252,11 +252,11 @@ } ], "primaryKey": { + "autoGenerate": false, "columnNames": [ "stream_id", "access_date" - ], - "autoGenerate": false + ] }, "indices": [ { @@ -301,10 +301,10 @@ } ], "primaryKey": { + "autoGenerate": false, "columnNames": [ "stream_id" - ], - "autoGenerate": false + ] }, "indices": [], "foreignKeys": [ @@ -351,10 +351,10 @@ } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "uid" - ], - "autoGenerate": true + ] }, "indices": [ { @@ -393,11 +393,11 @@ } ], "primaryKey": { + "autoGenerate": false, "columnNames": [ "playlist_id", "join_index" - ], - "autoGenerate": false + ] }, "indices": [ { @@ -493,10 +493,10 @@ } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "uid" - ], - "autoGenerate": true + ] }, "indices": [ { @@ -539,11 +539,11 @@ } ], "primaryKey": { + "autoGenerate": false, "columnNames": [ "stream_id", "subscription_id" - ], - "autoGenerate": false + ] }, "indices": [ { @@ -611,10 +611,10 @@ } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "uid" - ], - "autoGenerate": true + ] }, "indices": [ { @@ -647,11 +647,11 @@ } ], "primaryKey": { + "autoGenerate": false, "columnNames": [ "group_id", "subscription_id" - ], - "autoGenerate": false + ] }, "indices": [ { @@ -707,10 +707,10 @@ } ], "primaryKey": { + "autoGenerate": false, "columnNames": [ "subscription_id" - ], - "autoGenerate": false + ] }, "indices": [], "foreignKeys": [ @@ -726,12 +726,32 @@ ] } ] + }, + { + "tableName": "sponsorblock_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uploader` TEXT NOT NULL, PRIMARY KEY(`uploader`))", + "fields": [ + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uploader" + ] + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7dcdec7a500be9088f7a9a4767292b41')" ] } } \ No newline at end of file diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json index d4a89567b8..d4b71d6e18 100644 --- a/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/8.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 8, - "identityHash": "012fc8e7ad3333f1597347f34e76a513", + "identityHash": "7dcdec7a500be9088f7a9a4767292b41", "entities": [ { "tableName": "subscriptions", @@ -726,12 +726,32 @@ ] } ] + }, + { + "tableName": "sponsorblock_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uploader` TEXT NOT NULL, PRIMARY KEY(`uploader`))", + "fields": [ + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uploader" + ] + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7dcdec7a500be9088f7a9a4767292b41')" ] } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1127c55a4b..4726499f4e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ toggleServices()); - // If the current app name is bigger than the default "NewPipe" (7 chars), + // If the current app name is bigger than the default "Tubular" (7 chars), // let the text view grow a little more as well. - if (getString(R.string.app_name).length() > "NewPipe".length()) { + if (getString(R.string.app_name).length() > "Tubular".length()) { final ViewGroup.LayoutParams layoutParams = drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams(); layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 21c5354f44..ea1dbdf0c1 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -9,6 +9,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7; import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8; import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9; +import static org.schabi.newpipe.database.Migrations.MIGRATION_9_10; import android.content.Context; import android.database.Cursor; @@ -29,7 +30,7 @@ private static AppDatabase getDatabase(final Context context) { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, - MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) + MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10) .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt index 000b83953e..8a70fa9573 100644 --- a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt @@ -21,6 +21,7 @@ import com.grack.nanojson.JsonParserException import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.util.ReleaseVersionUtil +import org.schabi.newpipe.util.Version import java.io.IOException class NewVersionWorker( @@ -28,20 +29,15 @@ class NewVersionWorker( workerParams: WorkerParameters ) : Worker(context, workerParams) { - /** - * Method to compare the current and latest available app version. - * If a newer version is available, we show the update notification. - * - * @param versionName Name of new version - * @param apkLocationUrl Url with the new apk - * @param versionCode Code of new version - */ private fun compareAppVersionAndShowNotification( versionName: String, - apkLocationUrl: String?, - versionCode: Int + apkLocationUrl: String? ) { - if (BuildConfig.VERSION_CODE >= versionCode) { + val ourVersion = Version.fromString(BuildConfig.VERSION_NAME) + val theirVersion = Version.fromString(versionName) + + // abort if source version is the same or newer than target version + if (ourVersion >= theirVersion) { if (inputData.getBoolean(IS_MANUAL, false)) { // Show toast stating that the app is up-to-date if the update check was manual. ContextCompat.getMainExecutor(applicationContext).execute { @@ -62,7 +58,7 @@ class NewVersionWorker( ) val channelId = applicationContext.getString(R.string.app_update_notification_channel_id) val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId) - .setSmallIcon(R.drawable.ic_newpipe_update) + .setSmallIcon(R.drawable.ic_tubular_update) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setAutoCancel(true) .setContentIntent(pendingIntent) @@ -81,11 +77,6 @@ class NewVersionWorker( @Throws(IOException::class, ReCaptchaException::class) private fun checkNewVersion() { - // Check if the current apk is a github one or not. - if (!ReleaseVersionUtil.isReleaseApk) { - return - } - if (!inputData.getBoolean(IS_MANUAL, false)) { val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) // Check if the last request has happened a certain time ago @@ -118,19 +109,16 @@ class NewVersionWorker( // Parse the json from the response. try { - val newpipeVersionInfo = JsonParser.`object`() - .from(response.responseBody()).getObject("flavors") - .getObject("newpipe") - - val versionName = newpipeVersionInfo.getString("version") - val versionCode = newpipeVersionInfo.getInt("version_code") - val apkLocationUrl = newpipeVersionInfo.getString("apk") - compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode) + val jObj = JsonParser.`object`().from(response.responseBody()) + val versionName = jObj.getString("tag_name") + val apkLocationUrl = jObj + .getArray("assets") + .getObject(0) + .getString("browser_download_url") + compareAppVersionAndShowNotification(versionName, apkLocationUrl) } catch (e: JsonParserException) { - // Most likely something is wrong in data received from NEWPIPE_API_URL. - // Do not alarm user and fail silently. if (DEBUG) { - Log.w(TAG, "Could not get NewPipe API: invalid json", e) + Log.w(TAG, "Invalid json", e) } } } @@ -151,22 +139,9 @@ class NewVersionWorker( companion object { private val DEBUG = MainActivity.DEBUG private val TAG = NewVersionWorker::class.java.simpleName - private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json" + private const val NEWPIPE_API_URL = "https://api.github.com/repos/polymorphicshade/Tubular/releases/latest" private const val IS_MANUAL = "isManual" - /** - * Start a new worker which checks if all conditions for performing a version check are met, - * fetches the API endpoint [.NEWPIPE_API_URL] containing info about the latest NewPipe - * version and displays a notification about an available update if one is available. - *

- * Following conditions need to be met, before data is requested from the server: - * - * * The app is signed with the correct signing key (by TeamNewPipe / schabi). - * If the signing key differs from the one used upstream, the update cannot be installed. - * * The user enabled searching for and notifying about updates in the settings. - * * The app did not recently check for updates. - * We do not want to make unnecessary connections and DOS our servers. - */ @JvmStatic fun enqueueNewVersionCheckingWork(context: Context, isManual: Boolean) { val workRequest = OneTimeWorkRequestBuilder() diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index c59dc75323..c47d64182f 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -817,7 +817,8 @@ private void openDownloadDialog(final int currentServiceId, final String current inFlight(true); final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title); loadingDialog.show(getParentFragmentManager(), "loadingDialog"); - disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) + disposables.add(ExtractorHelper.getStreamInfo(getContext(), currentServiceId, + currentUrl, true) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(this::pleaseWait) @@ -837,7 +838,8 @@ private void openDownloadDialog(final int currentServiceId, final String current private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) { inFlight(true); - disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false) + disposables.add(ExtractorHelper.getStreamInfo(getContext(), currentServiceId, + currentUrl, false) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(this::pleaseWait) @@ -970,7 +972,8 @@ public void handleChoice(final Choice choice) { switch (choice.linkType) { case STREAM: - single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); + single = ExtractorHelper.getStreamInfo( + this, choice.serviceId, choice.url, false); userAction = UserAction.REQUESTED_STREAM; break; case CHANNEL: @@ -1062,7 +1065,7 @@ public void onDestroy() { private NotificationCompat.Builder createNotification() { return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle( getString(R.string.preferred_player_fetcher_notification_title)) diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index c9f630869c..c71b819123 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -27,6 +27,7 @@ public final class Migrations { public static final int DB_VER_7 = 7; public static final int DB_VER_8 = 8; public static final int DB_VER_9 = 9; + public static final int DB_VER_10 = 10; private static final String TAG = Migrations.class.getName(); public static final boolean DEBUG = MainActivity.DEBUG; @@ -238,15 +239,29 @@ public void migrate(@NonNull final SupportSQLiteDatabase database) { }; public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `sponsorblock_whitelist` " + + "(`uploader` TEXT NOT NULL, PRIMARY KEY(`uploader`));"); + } + }; + + public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) { @Override public void migrate(@NonNull final SupportSQLiteDatabase database) { database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"); database.execSQL("UPDATE search_history SET search = trim(search)"); + + // TODO: remove later - this meant to be a temporary fix for + // "rolling back" the database to fix poly's dumb mistake... + database.execSQL("DROP TABLE `sponsorblock_whitelist`"); + MIGRATION_9_10.migrate(database); } }; - public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) { + // TODO: change back to MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) + public static final Migration MIGRATION_9_10 = new Migration(DB_VER_9, DB_VER_10) { @Override public void migrate(@NonNull final SupportSQLiteDatabase database) { try { diff --git a/app/src/main/java/org/schabi/newpipe/database/sponsorblock/dao/SponsorBlockWhitelistDAO.java b/app/src/main/java/org/schabi/newpipe/database/sponsorblock/dao/SponsorBlockWhitelistDAO.java new file mode 100644 index 0000000000..cc1fe21309 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/sponsorblock/dao/SponsorBlockWhitelistDAO.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.database.sponsorblock.dao; + +import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.SPONSORBLOCK_WHITELIST_TABLE; +import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.UPLOADER; + +import androidx.room.Dao; +import androidx.room.Query; + +import org.schabi.newpipe.database.BasicDAO; + +import java.util.List; + +import io.reactivex.rxjava3.core.Flowable; + +@Dao +public abstract class SponsorBlockWhitelistDAO implements BasicDAO { + @Override + @Query("SELECT * FROM " + SPONSORBLOCK_WHITELIST_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + SPONSORBLOCK_WHITELIST_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(final int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + SPONSORBLOCK_WHITELIST_TABLE + " WHERE " + UPLOADER + " = :uploader") + public abstract int deleteByUploader(String uploader); + + @Query("SELECT 1 FROM " + SPONSORBLOCK_WHITELIST_TABLE + " WHERE " + UPLOADER + " = :uploader") + public abstract boolean exists(String uploader); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/sponsorblock/dao/SponsorBlockWhitelistEntry.java b/app/src/main/java/org/schabi/newpipe/database/sponsorblock/dao/SponsorBlockWhitelistEntry.java new file mode 100644 index 0000000000..97622f9f46 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/sponsorblock/dao/SponsorBlockWhitelistEntry.java @@ -0,0 +1,33 @@ +package org.schabi.newpipe.database.sponsorblock.dao; + +import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.SPONSORBLOCK_WHITELIST_TABLE; +import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.UPLOADER; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; + +@Entity(tableName = SPONSORBLOCK_WHITELIST_TABLE, + primaryKeys = {UPLOADER} +) +public class SponsorBlockWhitelistEntry { + public static final String SPONSORBLOCK_WHITELIST_TABLE = "sponsorblock_whitelist"; + public static final String UPLOADER = "uploader"; + + @NonNull + @ColumnInfo(name = UPLOADER) + private String uploader; + + public SponsorBlockWhitelistEntry(final @NonNull String uploader) { + this.uploader = uploader; + } + + @NonNull + public String getUploader() { + return uploader; + } + + public void setUploader(final @NonNull String uploader) { + this.uploader = uploader; + } +} 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 831a8cc4bb..1c095b4cea 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java @@ -64,11 +64,11 @@ public class ErrorActivity extends AppCompatActivity { // BUNDLE TAGS public static final String ERROR_INFO = "error_info"; - public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; + public static final String ERROR_EMAIL_ADDRESS = "polymorphicshade@gmail.com"; public static final String ERROR_EMAIL_SUBJECT = "Exception in "; public static final String ERROR_GITHUB_ISSUE_URL = - "https://github.com/TeamNewPipe/NewPipe/issues"; + "https://github.com/polymorphicshade/Tubular/issues"; public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); 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 6d8c1bd638..8243ec73ac 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -68,17 +68,32 @@ class ErrorInfo( // constructors with list of throwables constructor(throwable: List, userAction: UserAction, request: String) : - this(throwable, userAction, SERVICE_NONE, request) + this(removeDuplicates(throwable.toMutableList()), userAction, SERVICE_NONE, request) constructor(throwable: List, userAction: UserAction, request: String, serviceId: Int) : - this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request) + this(removeDuplicates(throwable.toMutableList()), userAction, ServiceHelper.getNameOfServiceById(serviceId), request) constructor(throwable: List, userAction: UserAction, request: String, info: Info?) : - this(throwable, userAction, getInfoServiceName(info), request) + this(removeDuplicates(throwable.toMutableList()), userAction, getInfoServiceName(info), request) companion object { const val SERVICE_NONE = "none" fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString()) + fun removeDuplicates(items: MutableList): List { + val messageCache = HashSet() + val iterator = items.listIterator() + while (iterator.hasNext()) { + val item = iterator.next() + val message = item.message + if (messageCache.contains(message)) { + iterator.remove() + } else { + messageCache.add(message) + } + } + return items + } + fun throwableListToStringList(throwableList: List) = throwableList.map { it.stackTraceToString() }.toTypedArray() diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java index 6ca66e0d2a..982f3a0491 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java @@ -30,7 +30,7 @@ public enum UserAction { DOWNLOAD_FAILED("download failed"), NEW_STREAMS_NOTIFICATIONS("new streams notifications"), PREFERENCES_MIGRATION("migration of preferences"), - SHARE_TO_NEWPIPE("share to newpipe"), + SHARE_TO_NEWPIPE("share to Tubular"), CHECK_FOR_NEW_APP_VERSION("check for new app version"), OPEN_INFO_ITEM_DIALOG("open info item dialog"); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java index fe4eef37ac..cd30198887 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java @@ -15,14 +15,14 @@ public class BlankFragment extends BaseFragment { @Override public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, final Bundle savedInstanceState) { - setTitle("NewPipe"); + setTitle("Tubular"); return inflater.inflate(R.layout.fragment_blank, container, false); } @Override public void onResume() { super.onResume(); - setTitle("NewPipe"); + setTitle("Tubular"); // leave this inline. Will make it harder for copy cats. // If you are a Copy cat FUCK YOU. // I WILL FIND YOU, AND I WILL ... 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 95b54f65a7..9f50a22b76 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 @@ -76,6 +76,10 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockExtractorHelper; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -86,11 +90,14 @@ import org.schabi.newpipe.fragments.EmptyFragment; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment; +import org.schabi.newpipe.fragments.list.sponsorblock.SponsorBlockFragment; +import org.schabi.newpipe.fragments.list.sponsorblock.SponsorBlockFragmentListener; 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.local.playlist.LocalPlaylistFragment; +import org.schabi.newpipe.local.sponsorblock.SponsorBlockDataManager; import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerType; @@ -111,7 +118,9 @@ import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.extractor.returnyoutubedislike.ReturnYouTubeDislikeInfo; import org.schabi.newpipe.util.PlayButtonHelper; +import org.schabi.newpipe.util.SponsorBlockMode; import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.external_communication.KoreUtils; @@ -129,6 +138,7 @@ import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -137,7 +147,8 @@ public final class VideoDetailFragment extends BaseStateFragment implements BackPressable, PlayerServiceExtendedEventListener, - OnKeyDownListener { + OnKeyDownListener, + SponsorBlockFragmentListener { public static final String KEY_SWITCHING_PLAYERS = "switching_players"; private static final float MAX_OVERLAY_ALPHA = 0.9f; @@ -157,6 +168,7 @@ public final class VideoDetailFragment private static final String COMMENTS_TAB_TAG = "COMMENTS"; private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"; + private static final String SPONSOR_BLOCK_TAB_TAG = "SPONSOR_BLOCK TAB"; private static final String EMPTY_TAB_TAG = "EMPTY TAB"; private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG"; @@ -165,6 +177,7 @@ public final class VideoDetailFragment private boolean showComments; private boolean showRelatedItems; private boolean showDescription; + private boolean showSponsorBlock; private String selectedTabTag; @AttrRes @NonNull @@ -176,18 +189,25 @@ public final class VideoDetailFragment private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener = - (sharedPreferences, key) -> { - if (getString(R.string.show_comments_key).equals(key)) { - showComments = sharedPreferences.getBoolean(key, true); - tabSettingsChanged = true; - } else if (getString(R.string.show_next_video_key).equals(key)) { - showRelatedItems = sharedPreferences.getBoolean(key, true); - tabSettingsChanged = true; - } else if (getString(R.string.show_description_key).equals(key)) { - showDescription = sharedPreferences.getBoolean(key, true); - tabSettingsChanged = true; - } - }; + this::onSharedPreferencesChanged; + private Disposable workerSponsorBlockModeCheck; + + private void onSharedPreferencesChanged(final SharedPreferences sharedPreferences, + final String key) { + if (getString(R.string.show_comments_key).equals(key)) { + showComments = sharedPreferences.getBoolean(key, true); + tabSettingsChanged = true; + } else if (getString(R.string.show_next_video_key).equals(key)) { + showRelatedItems = sharedPreferences.getBoolean(key, true); + tabSettingsChanged = true; + } else if (getString(R.string.show_description_key).equals(key)) { + showDescription = sharedPreferences.getBoolean(key, true); + tabSettingsChanged = true; + } else if (getString(R.string.sponsor_block_enable_key).equals(key)) { + showSponsorBlock = sharedPreferences.getBoolean(key, false); + tabSettingsChanged = true; + } + } @State protected int serviceId = Constants.NO_SERVICE_ID; @@ -205,6 +225,8 @@ public final class VideoDetailFragment int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED; @State protected boolean autoPlayEnabled = true; + @State + SponsorBlockMode currentSponsorBlockMode = null; @Nullable private StreamInfo currentInfo = null; @@ -213,6 +235,8 @@ public final class VideoDetailFragment private final CompositeDisposable disposables = new CompositeDisposable(); @Nullable private Disposable positionSubscriber = null; + private Disposable submitSegmentSubscriber; + private BottomSheetBehavior bottomSheetBehavior; private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback; @@ -231,6 +255,7 @@ public final class VideoDetailFragment private PlayerService playerService; private Player player; private final PlayerHolder playerHolder = PlayerHolder.getInstance(); + private SponsorBlockDataManager sponsorBlockDataManager; /*////////////////////////////////////////////////////////////////////////// // Service management @@ -310,6 +335,7 @@ public void onCreate(final Bundle savedInstanceState) { showComments = prefs.getBoolean(getString(R.string.show_comments_key), true); showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true); showDescription = prefs.getBoolean(getString(R.string.show_description_key), true); + showSponsorBlock = prefs.getBoolean(getString(R.string.sponsor_block_enable_key), false); selectedTabTag = prefs.getString( getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG); prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener); @@ -327,6 +353,8 @@ public void onChange(final boolean selfChange) { activity.getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, settingsContentObserver); + + sponsorBlockDataManager = new SponsorBlockDataManager(requireContext()); } @Override @@ -427,6 +455,17 @@ public void onDestroyView() { binding = null; } + @Override + public void onDetach() { + super.onDetach(); + if (submitSegmentSubscriber != null) { + submitSegmentSubscriber.dispose(); + } + if (workerSponsorBlockModeCheck != null) { + workerSponsorBlockModeCheck.dispose(); + } + } + @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -553,10 +592,10 @@ private void setOnLongClickListeners() { })); binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info -> - openBackgroundPlayer(true) + openBackgroundPlayer(true) )); binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info -> - openPopupPlayer(true) + openPopupPlayer(true) )); binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info -> NavigationHelper.openDownloads(activity))); @@ -839,7 +878,7 @@ private void startLoading(final boolean forceLoad, final boolean addToBackStack) private void runWorker(final boolean forceLoad, final boolean addToBackStack) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); - currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) + currentWorker = ExtractorHelper.getStreamInfo(getContext(), serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { @@ -902,6 +941,13 @@ private void initTabs() { tabContentDescriptions.add(R.string.description_tab_description); } + if (showSponsorBlock) { + // temp empty fragment. will be updated in handleResult + pageAdapter.addFragment(EmptyFragment.newInstance(false), SPONSOR_BLOCK_TAB_TAG); + tabIcons.add(R.drawable.ic_sponsor_block_enable); + tabContentDescriptions.add(R.string.sponsor_block_tab_description); + } + if (pageAdapter.getCount() == 0) { pageAdapter.addFragment(EmptyFragment.newInstance(true), EMPTY_TAB_TAG); } @@ -950,6 +996,28 @@ private void updateTabs(@NonNull final StreamInfo info) { pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info)); } + if (showSponsorBlock) { + final SponsorBlockFragment sponsorBlockFragment = new SponsorBlockFragment(info); + sponsorBlockFragment.setListener(this); + + pageAdapter.updateItem(SPONSOR_BLOCK_TAB_TAG, sponsorBlockFragment); + + workerSponsorBlockModeCheck = + sponsorBlockDataManager + .isWhiteListed(info.getUploaderName()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(isWhitelisted -> { + if (currentSponsorBlockMode == null) { + currentSponsorBlockMode = isWhitelisted + ? SponsorBlockMode.DISABLED + : SponsorBlockMode.ENABLED; + } + sponsorBlockFragment.setSponsorBlockMode(currentSponsorBlockMode); + sponsorBlockFragment.setIsWhitelisted(isWhitelisted); + }); + } + binding.viewPager.setVisibility(View.VISIBLE); // make sure the tab layout is visible updateTabLayoutVisibility(); @@ -1258,6 +1326,9 @@ private void tryAddVideoPlayerView() { playerUi.removeViewFromParent(); binding.playerPlaceholder.addView(playerUi.getBinding().getRoot()); playerUi.setupVideoSurfaceIfNeeded(); + if (currentInfo != null) { + playerUi.onMarkSeekbarRequested(currentInfo); + } } }); }); @@ -1480,6 +1551,8 @@ public void showLoading() { public void handleResult(@NonNull final StreamInfo info) { super.handleResult(info); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + currentInfo = info; setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), playQueue); @@ -1496,6 +1569,14 @@ public void handleResult(@NonNull final StreamInfo info) { displayUploaderAsSubChannel(info); } + final ReturnYouTubeDislikeInfo rydInfo = info.getRydInfo(); + final boolean isRydEnabled = prefs.getBoolean( + getString(R.string.return_youtube_dislike_enable_key), true); + final boolean overrideLikeCount = prefs.getBoolean( + getString(R.string.return_youtube_dislike_override_like_count_key), true); + final boolean overrideViewCount = prefs.getBoolean( + getString(R.string.return_youtube_dislike_override_view_count_key), true); + if (info.getViewCount() >= 0) { if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { binding.detailViewCountView.setText(Localization.listeningCount(activity, @@ -1512,6 +1593,12 @@ public void handleResult(@NonNull final StreamInfo info) { binding.detailViewCountView.setVisibility(View.GONE); } + // RYD override: views + if (rydInfo != null && isRydEnabled && overrideViewCount && rydInfo.viewCount > 0) { + binding.detailViewCountView.setText(Localization + .localizeViewCount(activity, rydInfo.viewCount)); + } + if (info.getDislikeCount() == -1 && info.getLikeCount() == -1) { binding.detailThumbsDownImgView.setVisibility(View.VISIBLE); binding.detailThumbsUpImgView.setVisibility(View.VISIBLE); @@ -1530,6 +1617,29 @@ public void handleResult(@NonNull final StreamInfo info) { binding.detailThumbsDownImgView.setVisibility(View.GONE); } + // RYD override: dislikes + if (rydInfo != null && isRydEnabled) { + final boolean showAsPercentage = prefs.getBoolean( + activity.getString( + R.string.return_youtube_dislike_show_dislikes_as_percentage_key), + false); + + final String dislikeText; + + if (showAsPercentage) { + final double percentage = + (double) rydInfo.dislikes / (rydInfo.likes + rydInfo.dislikes) * 100.0; + + dislikeText = Localization.localizePercentage(percentage); + } else { + dislikeText = Localization.shortCount(activity, rydInfo.dislikes); + } + + binding.detailThumbsDownCountView.setText(dislikeText); + binding.detailThumbsDownCountView.setVisibility(View.VISIBLE); + binding.detailThumbsDownImgView.setVisibility(View.VISIBLE); + } + if (info.getLikeCount() >= 0) { binding.detailThumbsUpCountView.setText(Localization.shortCount(activity, info.getLikeCount())); @@ -1539,6 +1649,15 @@ public void handleResult(@NonNull final StreamInfo info) { binding.detailThumbsUpCountView.setVisibility(View.GONE); binding.detailThumbsUpImgView.setVisibility(View.GONE); } + + // RYD override: likes + if (rydInfo != null && isRydEnabled && overrideLikeCount) { + binding.detailThumbsUpCountView.setText(Localization + .shortCount(activity, rydInfo.likes)); + binding.detailThumbsUpCountView.setVisibility(View.VISIBLE); + binding.detailThumbsUpImgView.setVisibility(View.VISIBLE); + } + binding.detailThumbsDisabledView.setVisibility(View.GONE); } @@ -1801,6 +1920,9 @@ public void onProgressUpdate(final int currentProgress, return; } + getSponsorBlockFragment().ifPresent( + fragment -> fragment.setCurrentProgress(currentProgress)); + if (player.getPlayQueue().getItem().getUrl().equals(url)) { updatePlaybackProgress(currentProgress, duration); } @@ -1808,6 +1930,34 @@ public void onProgressUpdate(final int currentProgress, @Override public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { + final Context context = requireContext(); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final boolean isSponsorBlockEnabled = + prefs.getBoolean(getString(R.string.sponsor_block_enable_key), false); + + if (player != null && isSponsorBlockEnabled) { + workerSponsorBlockModeCheck = + sponsorBlockDataManager + .isWhiteListed(info.getUploaderName()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(isWhitelisted -> { + if (currentSponsorBlockMode == null) { + currentSponsorBlockMode = isWhitelisted + ? SponsorBlockMode.DISABLED + : SponsorBlockMode.ENABLED; + } + if (player != null) { + player.setSponsorBlockMode(currentSponsorBlockMode); + } + getSponsorBlockFragment().ifPresent( + fragment -> { + fragment.setSponsorBlockMode(currentSponsorBlockMode); + fragment.setIsWhitelisted(isWhitelisted); + }); + }); + } final StackItem item = findQueueInStack(queue); if (item != null) { // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue) @@ -2466,4 +2616,153 @@ private void updateBottomSheetState(final int newState) { lastStableBottomSheetState = newState; } } + + private Optional getSponsorBlockFragment() { + final int sponsorBlockTabPos = pageAdapter.getItemPositionByTitle(SPONSOR_BLOCK_TAB_TAG); + + if (sponsorBlockTabPos < 0) { + return Optional.empty(); + } + + final Fragment fragment = pageAdapter.getItem(sponsorBlockTabPos); + + if (fragment instanceof SponsorBlockFragment sponsorBlockFragment) { + return Optional.of(sponsorBlockFragment); + } else { + return Optional.empty(); + } + } + + @Override + public void onSkippingEnabledChanged(final boolean newValue) { + if (player == null) { + return; + } + + currentSponsorBlockMode = newValue + ? SponsorBlockMode.ENABLED + : SponsorBlockMode.DISABLED; + + player.setSponsorBlockMode(currentSponsorBlockMode); + } + + @Override + public void onRequestNewPendingSegment(final int startTime, final int endTime) { + if (currentInfo == null) { + return; + } + + if (player == null) { + return; + } + + currentInfo.removeSponsorBlockSegment("TEMP"); + + final SponsorBlockSegment segment = new SponsorBlockSegment( + "TEMP", + startTime, + endTime, + SponsorBlockCategory.PENDING, + SponsorBlockAction.SKIP); + + currentInfo.addSponsorBlockSegment(segment); + + player.UIs().get(MainPlayerUi.class).ifPresent( + playerUi -> playerUi.onMarkSeekbarRequested(currentInfo)); + + getSponsorBlockFragment().ifPresent(SponsorBlockFragment::refreshSponsorBlockSegments); + } + + @Override + public void onRequestClearPendingSegment() { + if (currentInfo == null) { + return; + } + + if (player == null) { + return; + } + + currentInfo.removeSponsorBlockSegment("TEMP"); + + player.UIs().get(MainPlayerUi.class).ifPresent( + playerUi -> playerUi.onMarkSeekbarRequested(currentInfo)); + + getSponsorBlockFragment().ifPresent(SponsorBlockFragment::refreshSponsorBlockSegments); + } + + @Override + public void onRequestSubmitPendingSegment(final SponsorBlockSegment newSegment) { + if (currentInfo == null) { + return; + } + + if (player == null) { + return; + } + + final Context context = requireContext(); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String apiUrl = prefs.getString(context + .getString(R.string.sponsor_block_api_url_key), null); + if (apiUrl == null || apiUrl.isEmpty()) { + return; + } + + submitSegmentSubscriber = Single.fromCallable(() -> + SponsorBlockExtractorHelper.submitSponsorBlockSegment( + currentInfo, + newSegment, + apiUrl)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + final int responseCode = response.responseCode(); + + // 200 = all good + // 409 = all good, but the request timed out + if (responseCode != 200 && responseCode != 409) { + String message = response.responseMessage(); + if (message.equals("")) { + message = "Error " + responseCode; + } + Toast.makeText(context, + message, + Toast.LENGTH_SHORT).show(); + return; + } + + currentInfo.removeSponsorBlockSegment("TEMP"); + currentInfo.addSponsorBlockSegment(newSegment); + + player.UIs().get(MainPlayerUi.class).ifPresent( + playerUi -> playerUi.onMarkSeekbarRequested(currentInfo)); + + getSponsorBlockFragment().ifPresent( + SponsorBlockFragment::clearPendingSegment); + + new AlertDialog + .Builder(context) + .setMessage(R.string.sponsor_block_upload_success_message) + .setPositiveButton(R.string.ok, (d, w) -> d.dismiss()) + .show(); + }, throwable -> { + if (throwable instanceof NullPointerException) { + return; + } + ErrorUtil.showSnackbar(context, + new ErrorInfo(throwable, UserAction.USER_REPORT, + "Submit SponsorBlock segment")); + }); + } + + @Override + public void onSeekToRequested(final long positionMillis) { + if (player == null) { + return; + } + + player.seekTo(positionMillis); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index dd5eb6c8ab..0f906b2701 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -246,7 +246,7 @@ public void handleResult(@NonNull final L result) { errors.removeIf(ContentNotSupportedException.class::isInstance); if (!errors.isEmpty()) { - dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), + dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(errors, errorUserAction, "Start loading: " + url, serviceId)); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 95ac42eed0..d5518b5536 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -28,6 +28,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.PlayButtonHelper; +import java.util.Collections; import java.util.List; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -165,4 +166,17 @@ public PlayQueue getPlayQueue() { return new ChannelTabPlayQueue(currentInfo.getServiceId(), tabHandler, currentInfo.getNextPage(), streamItems, 0); } + + @Override + public PlayQueue getShuffledQueue() { + final List streamItems = infoListAdapter.getItemsList().stream() + .filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast) + .collect(Collectors.toList()); + + Collections.shuffle(streamItems); + + return new ChannelTabPlayQueue(currentInfo.getServiceId(), + tabHandler, currentInfo.getNextPage(), streamItems, 0); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java index e4705bb718..6d13a20982 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java @@ -8,4 +8,6 @@ */ public interface PlaylistControlViewHolder { PlayQueue getPlayQueue(); + + PlayQueue getShuffledQueue(); } 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 9afb063441..61a1527f0e 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 @@ -57,6 +57,7 @@ import org.schabi.newpipe.util.text.TextEllipsizer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -259,6 +260,9 @@ public boolean onOptionsItemSelected(final MenuItem item) { )); } break; + case R.id.menu_item_playlist_shuffle_all: + NavigationHelper.playOnMainPlayer(activity, getShuffledQueue()); + break; default: return super.onOptionsItemSelected(item); } @@ -375,13 +379,27 @@ public PlayQueue getPlayQueue() { return getPlayQueue(0); } + @Override + public PlayQueue getShuffledQueue() { + return getPlayQueue(0, true); + } + private PlayQueue getPlayQueue(final int index) { + return getPlayQueue(index, false); + } + + private PlayQueue getPlayQueue(final int index, final boolean shuffled) { final List infoItems = new ArrayList<>(); for (final InfoItem i : infoListAdapter.getItemsList()) { if (i instanceof StreamInfoItem) { infoItems.add((StreamInfoItem) i); } } + + if (shuffled) { + Collections.shuffle(infoItems); + } + return new PlaylistPlayQueue( currentInfo.getServiceId(), currentInfo.getUrl(), diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockFragment.java new file mode 100644 index 0000000000..d42b758e9c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockFragment.java @@ -0,0 +1,359 @@ +package org.schabi.newpipe.fragments.list.sponsorblock; + +import static org.schabi.newpipe.util.TimeUtils.millisecondsToString; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import org.schabi.newpipe.BaseFragment; +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.FragmentSponsorBlockBinding; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.local.sponsorblock.SponsorBlockDataManager; +import org.schabi.newpipe.util.SponsorBlockHelper; +import org.schabi.newpipe.util.SponsorBlockMode; + +import icepick.State; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class SponsorBlockFragment + extends BaseFragment + implements CompoundButton.OnCheckedChangeListener, + SponsorBlockSegmentListAdapterListener { + @State + StreamInfo streamInfo = null; + FragmentSponsorBlockBinding binding; + private Integer markedStartTime = null; + private Integer markedEndTime = null; + private SponsorBlockSegmentListAdapter segmentListAdapter; + private int currentProgress = -1; + private @Nullable SponsorBlockFragmentListener sponsorBlockFragmentListener; + private SponsorBlockDataManager sponsorBlockDataManager; + private Disposable workerAddToWhitelisted; + private Disposable workerRemoveFromWhitelisted; + private SponsorBlockMode currentSponsorBlockMode = null; + private boolean currentIsWhitelisted; + + public SponsorBlockFragment() { + } + + public SponsorBlockFragment(@NonNull final StreamInfo streamInfo) { + this.streamInfo = streamInfo; + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + sponsorBlockDataManager = new SponsorBlockDataManager(getContext()); + } + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + + if (streamInfo == null) { + return; + } + + segmentListAdapter = new SponsorBlockSegmentListAdapter(context, this); + segmentListAdapter.setItems(streamInfo.getSponsorBlockSegments()); + } + + @Override + public void onDetach() { + super.onDetach(); + + if (workerAddToWhitelisted != null) { + workerAddToWhitelisted.dispose(); + } + if (workerRemoveFromWhitelisted != null) { + workerRemoveFromWhitelisted.dispose(); + } + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + if (sponsorBlockDataManager != null) { + sponsorBlockDataManager = new SponsorBlockDataManager(getContext()); + } + + binding = FragmentSponsorBlockBinding.inflate(inflater, container, false); + + binding.sponsorBlockControlsMarkSegmentStart.setOnClickListener(v -> + doMarkPendingSegment(true)); + binding.sponsorBlockControlsMarkSegmentEnd.setOnClickListener(v -> + doMarkPendingSegment(false)); + binding.sponsorBlockControlsSegmentStart.setOnClickListener(v -> + doPendingSegmentSeek(true)); + binding.sponsorBlockControlsSegmentEnd.setOnClickListener(v -> + doPendingSegmentSeek(false)); + binding.sponsorBlockControlsClearSegment.setOnClickListener(v -> + doClearPendingSegment()); + binding.sponsorBlockControlsSubmitSegment.setOnClickListener(v -> + doSubmitPendingSegment()); + + binding.segmentList.setAdapter(segmentListAdapter); + + binding.skippingIsEnabledSwitch.setChecked( + currentSponsorBlockMode == SponsorBlockMode.ENABLED); + + binding.channelIsWhitelistedSwitch.setChecked(currentIsWhitelisted); + + if (currentIsWhitelisted) { + binding.skippingIsEnabledSwitch.setChecked(false); + binding.skippingIsEnabledSwitch.setEnabled(!currentIsWhitelisted); + } + + binding.skippingIsEnabledSwitch.setOnCheckedChangeListener(this); + binding.channelIsWhitelistedSwitch.setOnCheckedChangeListener(this); + + return binding.getRoot(); + } + + @Override + public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) { + if (buttonView.getId() == R.id.skipping_is_enabled_switch) { + if (sponsorBlockFragmentListener != null) { + sponsorBlockFragmentListener.onSkippingEnabledChanged(isChecked); + } + } else if (buttonView.getId() == R.id.channel_is_whitelisted_switch) { + final Context context = requireContext(); + + final String toastText; + + if (isChecked) { + toastText = context.getString( + R.string.sponsor_block_uploader_added_to_whitelist_toast); + + workerAddToWhitelisted = + sponsorBlockDataManager.addToWhitelist(streamInfo.getUploaderName()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + Toast.makeText(context, toastText, Toast.LENGTH_LONG).show(); + }, error -> { + // TODO + }); + } else { + toastText = context.getString( + R.string.sponsor_block_uploader_removed_from_whitelist_toast); + + workerRemoveFromWhitelisted = + sponsorBlockDataManager.removeFromWhitelist(streamInfo.getUploaderName()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + Toast.makeText(context, toastText, Toast.LENGTH_LONG).show(); + }, error -> { + // TODO + }); + } + + binding.skippingIsEnabledSwitch.setChecked(false); + binding.skippingIsEnabledSwitch.setEnabled(!isChecked); + } + } + + public void setListener(final SponsorBlockFragmentListener listener) { + sponsorBlockFragmentListener = listener; + } + + public void setSponsorBlockMode(@NonNull final SponsorBlockMode mode) { + currentSponsorBlockMode = mode; + + if (binding == null) { + return; + } + + binding.skippingIsEnabledSwitch.setOnCheckedChangeListener(null); + binding.skippingIsEnabledSwitch.setChecked(mode == SponsorBlockMode.ENABLED); + binding.skippingIsEnabledSwitch.setOnCheckedChangeListener(this); + } + + public void setIsWhitelisted(final boolean value) { + currentIsWhitelisted = value; + + if (binding == null) { + return; + } + + binding.channelIsWhitelistedSwitch.setOnCheckedChangeListener(null); + binding.channelIsWhitelistedSwitch.setChecked(value); + binding.channelIsWhitelistedSwitch.setOnCheckedChangeListener(this); + + if (value) { + binding.skippingIsEnabledSwitch.setOnCheckedChangeListener(null); + binding.skippingIsEnabledSwitch.setChecked(false); + binding.skippingIsEnabledSwitch.setOnCheckedChangeListener(this); + + binding.skippingIsEnabledSwitch.setEnabled(!currentIsWhitelisted); + } + } + + public void setCurrentProgress(final int progress) { + currentProgress = progress; + } + + @SuppressLint("SetTextI18n") + public void clearPendingSegment() { + markedStartTime = null; + markedEndTime = null; + + binding.sponsorBlockControlsSegmentStart.setText("00:00:00"); + binding.sponsorBlockControlsSegmentEnd.setText("00:00:00"); + + if (sponsorBlockFragmentListener != null) { + sponsorBlockFragmentListener.onRequestClearPendingSegment(); + } + } + + public void refreshSponsorBlockSegments() { + if (segmentListAdapter == null) { + return; + } + + segmentListAdapter.setItems(streamInfo.getSponsorBlockSegments()); + } + + private void doMarkPendingSegment(final boolean isStart) { + if (currentProgress < 0) { + return; + } + + if (isStart) { + if (markedEndTime != null && currentProgress > markedEndTime) { + Toast.makeText(getContext(), + getString(R.string.sponsor_block_invalid_start_toast), + Toast.LENGTH_SHORT).show(); + return; + } + markedStartTime = currentProgress; + } else { + if (markedStartTime != null && currentProgress < markedStartTime) { + Toast.makeText(getContext(), + getString(R.string.sponsor_block_invalid_end_toast), + Toast.LENGTH_SHORT).show(); + return; + } + markedEndTime = currentProgress; + } + + if (markedStartTime != null) { + binding.sponsorBlockControlsSegmentStart.setText( + millisecondsToString(markedStartTime)); + } + + if (markedEndTime != null) { + binding.sponsorBlockControlsSegmentEnd.setText( + millisecondsToString(markedEndTime)); + } + + if (markedStartTime != null && markedEndTime != null) { + if (sponsorBlockFragmentListener != null) { + sponsorBlockFragmentListener.onRequestNewPendingSegment( + markedStartTime, markedEndTime); + } + } + + final String message = isStart + ? getString(R.string.sponsor_block_marked_start_toast) + : getString(R.string.sponsor_block_marked_end_toast); + Toast.makeText(getContext(), + message, + Toast.LENGTH_SHORT).show(); + } + + @SuppressLint("SetTextI18n") + private void doClearPendingSegment() { + new AlertDialog + .Builder(requireContext()) + .setMessage(R.string.sponsor_block_clear_marked_segment_prompt) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setPositiveButton(R.string.yes, (dialog, which) -> { + clearPendingSegment(); + dialog.dismiss(); + }) + .show(); + } + + private void doPendingSegmentSeek(final boolean isStart) { + if (isStart && markedStartTime != null) { + onSkipToTimestampRequested((long) markedStartTime); + } else if (markedEndTime != null) { + onSkipToTimestampRequested((long) markedEndTime); + } + } + + private void doSubmitPendingSegment() { + final Context context = requireContext(); + + if (markedStartTime == null || markedEndTime == null) { + Toast.makeText(context, + getString(R.string.sponsor_block_missing_times_toast), + Toast.LENGTH_SHORT).show(); + return; + } + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.sponsor_block_select_a_category); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.setItems(new String[]{ + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.SPONSOR), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.INTRO), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.OUTRO), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.INTERACTION), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.HIGHLIGHT), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.SELF_PROMO), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.NON_MUSIC), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.PREVIEW), + SponsorBlockHelper.convertCategoryToFriendlyName( + context, SponsorBlockCategory.FILLER) + }, (dialog, which) -> { + final SponsorBlockCategory category = SponsorBlockCategory.values()[which]; + final SponsorBlockAction action = category == SponsorBlockCategory.HIGHLIGHT + ? SponsorBlockAction.POI + : SponsorBlockAction.SKIP; + final SponsorBlockSegment newSegment = + new SponsorBlockSegment( + "", markedStartTime, markedEndTime, category, action); + if (sponsorBlockFragmentListener != null) { + sponsorBlockFragmentListener.onRequestSubmitPendingSegment(newSegment); + } + dialog.dismiss(); + }); + builder.show(); + } + + @Override + public void onSkipToTimestampRequested(final long positionMillis) { + if (sponsorBlockFragmentListener != null) { + sponsorBlockFragmentListener.onSeekToRequested(positionMillis); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockFragmentListener.java b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockFragmentListener.java new file mode 100644 index 0000000000..cdfc605cb6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockFragmentListener.java @@ -0,0 +1,11 @@ +package org.schabi.newpipe.fragments.list.sponsorblock; + +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment; + +public interface SponsorBlockFragmentListener { + void onSkippingEnabledChanged(boolean newValue); + void onRequestNewPendingSegment(int startTime, int endTime); + void onRequestClearPendingSegment(); + void onRequestSubmitPendingSegment(SponsorBlockSegment newSegment); + void onSeekToRequested(long positionMillis); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockSegmentListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockSegmentListAdapter.java new file mode 100644 index 0000000000..a06fa68c9f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockSegmentListAdapter.java @@ -0,0 +1,294 @@ +package org.schabi.newpipe.fragments.list.sponsorblock; + +import static org.schabi.newpipe.util.TimeUtils.millisecondsToString; + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +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.extractor.sponsorblock.SponsorBlockCategory; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockExtractorHelper; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment; +import org.schabi.newpipe.util.SponsorBlockHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class SponsorBlockSegmentListAdapter extends + RecyclerView.Adapter { + private final Context context; + private ArrayList sponsorBlockSegments = new ArrayList<>(); + private final SponsorBlockSegmentListAdapterListener listener; + + public SponsorBlockSegmentListAdapter(final Context context, + final SponsorBlockSegmentListAdapterListener listener) { + this.context = context; + this.listener = listener; + } + + public void setItems(final SponsorBlockSegment[] items) { + if (items == null) { + sponsorBlockSegments.clear(); + } else { + sponsorBlockSegments = new ArrayList<>(Arrays.asList(items)); + } + + // find the first "highlight" segment (if it exists) and move it to the top + if (sponsorBlockSegments.size() > 0) { + final Optional highlightSegment = + sponsorBlockSegments + .stream() + .filter(x -> x.category == SponsorBlockCategory.HIGHLIGHT) + .findFirst(); + + if (highlightSegment.isPresent()) { + sponsorBlockSegments.remove(highlightSegment.get()); + sponsorBlockSegments.add(0, highlightSegment.get()); + } + } + + notifyDataSetChanged(); + } + + @NonNull + @Override + public SponsorBlockSegmentListAdapter.SponsorBlockSegmentItemViewHolder onCreateViewHolder( + @NonNull final ViewGroup parent, final int viewType) { + final View itemView = LayoutInflater + .from(context) + .inflate(R.layout.list_segments_item, parent, false); + return new SponsorBlockSegmentItemViewHolder(itemView, listener); + } + + @Override + public void onBindViewHolder( + @NonNull final SponsorBlockSegmentListAdapter.SponsorBlockSegmentItemViewHolder holder, + final int position) { + final SponsorBlockSegment sponsorBlockSegment = sponsorBlockSegments.get(position); + holder.updateFrom(sponsorBlockSegment); + } + + @Override + public int getItemCount() { + return sponsorBlockSegments.size(); + } + + public static class SponsorBlockSegmentItemViewHolder extends RecyclerView.ViewHolder { + private final View itemSegmentColorView; + private final ImageView itemSegmentSkipToHighlight; + private final TextView itemSegmentNameTextView; + private final TextView itemSegmentStartTimeTextView; + private final TextView itemSegmentEndTimeTextView; + private final ImageView itemSegmentVoteUpImageView; + private final ImageView itemSegmentVoteDownImageView; + private Disposable voteSubscriber; + private String segmentUuid; + private boolean isVoting; + private boolean hasUpVoted; + private boolean hasDownVoted; + private boolean hasResetVote; + private SponsorBlockSegment currentSponsorBlockSegment; + + public SponsorBlockSegmentItemViewHolder( + @NonNull final View itemView, + final SponsorBlockSegmentListAdapterListener listener) { + super(itemView); + + itemSegmentColorView = itemView.findViewById(R.id.item_segment_color_view); + itemSegmentSkipToHighlight = itemView.findViewById(R.id.item_segment_skip_to_highlight); + itemSegmentSkipToHighlight.setOnClickListener(v -> { + if (currentSponsorBlockSegment != null && listener != null) { + listener.onSkipToTimestampRequested( + (long) currentSponsorBlockSegment.startTime); + } + }); + itemSegmentNameTextView = itemView.findViewById( + R.id.item_segment_category_name_textview); + itemSegmentStartTimeTextView = itemView.findViewById( + R.id.item_segment_start_time_textview); + itemSegmentStartTimeTextView.setOnClickListener(v -> { + if (currentSponsorBlockSegment != null && listener != null) { + listener.onSkipToTimestampRequested( + (long) currentSponsorBlockSegment.startTime); + } + }); + itemSegmentEndTimeTextView = itemView.findViewById(R.id.item_segment_end_time_textview); + itemSegmentEndTimeTextView.setOnClickListener(v -> { + if (currentSponsorBlockSegment != null && listener != null) { + listener.onSkipToTimestampRequested((long) currentSponsorBlockSegment.endTime); + } + }); + + // voting: + // 1 = up + // 0 = down + // 20 = reset + itemSegmentVoteUpImageView = + itemView.findViewById(R.id.item_segment_vote_up_imageview); + itemSegmentVoteUpImageView.setOnClickListener(v -> vote(1)); + itemSegmentVoteUpImageView.setOnLongClickListener(v -> { + vote(20); + return true; + }); + itemSegmentVoteDownImageView = + itemView.findViewById(R.id.item_segment_vote_down_imageview); + itemSegmentVoteDownImageView.setOnClickListener(v -> vote(0)); + itemSegmentVoteDownImageView.setOnLongClickListener(v -> { + vote(20); + return true; + }); + } + + private void updateFrom(final SponsorBlockSegment sponsorBlockSegment) { + currentSponsorBlockSegment = sponsorBlockSegment; + + final Context context = itemView.getContext(); + + // uuid + segmentUuid = sponsorBlockSegment.uuid; + + // category color + final Integer segmentColor = + SponsorBlockHelper.convertCategoryToColor( + sponsorBlockSegment.category, context); + if (segmentColor != null) { + itemSegmentColorView.setBackgroundColor(segmentColor); + } + + // skip to highlight + if (sponsorBlockSegment.category == SponsorBlockCategory.HIGHLIGHT) { + itemSegmentColorView.setVisibility(View.GONE); + itemSegmentSkipToHighlight.setVisibility(View.VISIBLE); + } else { + itemSegmentColorView.setVisibility(View.VISIBLE); + itemSegmentSkipToHighlight.setVisibility(View.GONE); + } + + // category name + final String friendlyCategoryName = + SponsorBlockHelper.convertCategoryToFriendlyName( + context, sponsorBlockSegment.category); + itemSegmentNameTextView.setText(friendlyCategoryName); + + // from + final String startText = millisecondsToString(sponsorBlockSegment.startTime); + itemSegmentStartTimeTextView.setText(startText); + + // to + final String endText = millisecondsToString(sponsorBlockSegment.endTime); + itemSegmentEndTimeTextView.setText(endText); + + if (sponsorBlockSegment.category == SponsorBlockCategory.PENDING + || sponsorBlockSegment.uuid.equals("TEMP") + || sponsorBlockSegment.uuid.equals("")) { + itemSegmentVoteUpImageView.setVisibility(View.INVISIBLE); + itemSegmentVoteDownImageView.setVisibility(View.INVISIBLE); + } + } + + private void vote(final int value) { + if (segmentUuid == null) { + return; + } + + if (isVoting) { + return; + } + + if (voteSubscriber != null) { + voteSubscriber.dispose(); + } + + // these 3 checks prevent the user from continuously spamming votes + // (not entirely sure if we need this) + + if (value == 0 && hasDownVoted) { + return; + } + + if (value == 1 && hasUpVoted) { + return; + } + + if (value == 20 && hasResetVote) { + return; + } + + final Context context = itemView.getContext(); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String apiUrl = prefs.getString(context + .getString(R.string.sponsor_block_api_url_key), null); + if (apiUrl == null || apiUrl.isEmpty()) { + return; + } + + voteSubscriber = Single.fromCallable(() -> { + isVoting = true; + return SponsorBlockExtractorHelper.submitSponsorBlockSegmentVote( + segmentUuid, apiUrl, value); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + isVoting = false; + String toastMessage; + if (response.responseCode() != 200) { + toastMessage = response.responseMessage(); + if (toastMessage.equals("")) { + toastMessage = "Error " + response.responseCode(); + } + } else if (value == 0) { + hasDownVoted = true; + hasUpVoted = false; + hasResetVote = false; + toastMessage = context.getString( + R.string.sponsor_block_segment_voted_down_toast); + } else if (value == 1) { + hasDownVoted = false; + hasUpVoted = true; + hasResetVote = false; + toastMessage = context.getString( + R.string.sponsor_block_segment_voted_up_toast); + } else if (value == 20) { + hasDownVoted = false; + hasUpVoted = false; + hasResetVote = true; + toastMessage = context.getString( + R.string.sponsor_block_segment_reset_vote_toast); + } else { + return; + } + Toast.makeText(context, + toastMessage, + Toast.LENGTH_SHORT).show(); + }, throwable -> { + if (throwable instanceof NullPointerException) { + return; + } + ErrorUtil.showSnackbar(context, + new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE, + "Submit vote for SponsorBlock segment")); + }); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockSegmentListAdapterListener.java b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockSegmentListAdapterListener.java new file mode 100644 index 0000000000..3939e9eff7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/sponsorblock/SponsorBlockSegmentListAdapterListener.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.fragments.list.sponsorblock; + +public interface SponsorBlockSegmentListAdapterListener { + void onSkipToTimestampRequested(long positionMillis); +} 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 8ea89368d6..8b55ef9fcb 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 @@ -52,7 +52,7 @@ class NotificationHelper(val context: Context) { .setNumber(newStreams.size) .setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE) .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setColor(ContextCompat.getColor(context, R.color.ic_launcher_background)) .setColorized(true) .setAutoCancel(true) @@ -135,7 +135,7 @@ class NotificationHelper(val context: Context) { context, context.getString(R.string.streams_notification_channel_id) ) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setLargeIcon(channelIcon) .setContentTitle(item.name) .setContentText(item.uploaderName) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt index a40bf35dc5..119ca8c348 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt @@ -78,7 +78,7 @@ class NotificationWorker( applicationContext.getString(R.string.notification_channel_id) ).setOngoing(true) .setProgress(-1, -1, true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_LOW) .setContentTitle(applicationContext.getString(R.string.feed_notification_loading)) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index f960040de6..94703f65b4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -140,7 +140,7 @@ class FeedLoadService : Service() { return NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setProgress(-1, -1, true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .addAction(0, getString(R.string.cancel), cancelActionIntent) .setContentTitle(getString(R.string.feed_notification_loading)) 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 ed3cf548f9..9be9a90343 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 @@ -59,6 +59,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers; public class HistoryRecordManager { + private final Context context; private final AppDatabase database; private final StreamDAO streamTable; private final StreamHistoryDAO streamHistoryTable; @@ -69,6 +70,7 @@ public class HistoryRecordManager { private final String streamHistoryKey; public HistoryRecordManager(final Context context) { + this.context = context; database = NewPipeDatabase.getInstance(context); streamTable = database.streamDAO(); streamHistoryTable = database.streamHistoryDAO(); @@ -103,6 +105,7 @@ public Maybe markAsWatched(final StreamInfoItem info) { // Duration will not exist if the item was loaded with fast mode, so fetch it if empty if (info.getDuration() < 0) { final StreamInfo completeInfo = ExtractorHelper.getStreamInfo( + context, info.getServiceId(), info.getUrl(), false @@ -235,7 +238,7 @@ private boolean isSearchHistoryEnabled() { /////////////////////////////////////////////////////// public Maybe loadStreamState(final PlayQueueItem queueItem) { - return queueItem.getStream() + return queueItem.getStream(context) .map(info -> streamTable.upsert(new StreamEntity(info))) .flatMapPublisher(streamStateTable::getState) .firstElement() 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 1fea7e1559..f634765305 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 @@ -372,7 +372,16 @@ public PlayQueue getPlayQueue() { return getPlayQueue(0); } + @Override + public PlayQueue getShuffledQueue() { + return getPlayQueue(0, true); + } + private PlayQueue getPlayQueue(final int index) { + return getPlayQueue(index, false); + } + + private PlayQueue getPlayQueue(final int index, final boolean shuffled) { if (itemListAdapter == null) { return new SinglePlayQueue(Collections.emptyList(), 0); } @@ -384,6 +393,11 @@ private PlayQueue getPlayQueue(final int index) { streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); } } + + if (shuffled) { + Collections.shuffle(streamInfoItems); + } + return new SinglePlayQueue(streamInfoItems, index); } 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 d5ae431fad..9f336bd675 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 @@ -377,6 +377,8 @@ public boolean onOptionsItemSelected(final MenuItem item) { if (!isRewritingPlaylist) { openRemoveDuplicatesDialog(); } + } else if (item.getItemId() == R.id.menu_item_playlist_shuffle_all) { + NavigationHelper.playOnMainPlayer(activity, getShuffledQueue()); } else { return super.onOptionsItemSelected(item); } @@ -847,7 +849,16 @@ public PlayQueue getPlayQueue() { return getPlayQueue(0); } + @Override + public PlayQueue getShuffledQueue() { + return getPlayQueue(0, true); + } + private PlayQueue getPlayQueue(final int index) { + return getPlayQueue(index, false); + } + + private PlayQueue getPlayQueue(final int index, final boolean shuffled) { if (itemListAdapter == null) { return new SinglePlayQueue(Collections.emptyList(), 0); } @@ -859,6 +870,11 @@ private PlayQueue getPlayQueue(final int index) { streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem()); } } + + if (shuffled) { + Collections.shuffle(streamInfoItems); + } + return new SinglePlayQueue(streamInfoItems, index); } diff --git a/app/src/main/java/org/schabi/newpipe/local/sponsorblock/SponsorBlockDataManager.java b/app/src/main/java/org/schabi/newpipe/local/sponsorblock/SponsorBlockDataManager.java new file mode 100644 index 0000000000..da592c6b2e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/sponsorblock/SponsorBlockDataManager.java @@ -0,0 +1,43 @@ +package org.schabi.newpipe.local.sponsorblock; + +import android.content.Context; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistDAO; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; + +public class SponsorBlockDataManager { + private final SponsorBlockWhitelistDAO sponsorBlockWhitelistTable = null; + + public SponsorBlockDataManager(final Context context) { + final AppDatabase database = NewPipeDatabase.getInstance(context); + } + + public Maybe addToWhitelist(final String uploader) { +// return Maybe.fromCallable(() -> { +// final SponsorBlockWhitelistEntry entry = new SponsorBlockWhitelistEntry(uploader); +// return sponsorBlockWhitelistTable.insert(entry); +// }).subscribeOn(Schedulers.io()); + return Maybe.empty(); + } + + public Completable removeFromWhitelist(final String uploader) { +// return Completable.fromAction(() -> sponsorBlockWhitelistTable.deleteByUploader(uploader)); + return Completable.complete(); + } + + public Single isWhiteListed(final String uploader) { +// return Single.fromCallable(() -> sponsorBlockWhitelistTable.exists(uploader)) +// .subscribeOn(Schedulers.io()); + return Single.just(false); + } + + public Completable clearWhitelist() { +// return Completable.fromAction(sponsorBlockWhitelistTable::deleteAll); + return Completable.complete(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java index b7c11b1605..daa623861c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java @@ -169,7 +169,7 @@ protected void postErrorResult(final String title, final String text) { final String textOrEmpty = text == null ? "" : text; notificationBuilder = new NotificationCompat .Builder(this, getString(R.string.notification_channel_id)) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle(title) .setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty)) @@ -181,7 +181,7 @@ protected NotificationCompat.Builder createNotification() { return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setProgress(-1, -1, true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle(getString(getTitle())); } 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 c012f6008c..f4faa1c105 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -46,6 +46,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ServiceHelper; +import org.schabi.newpipe.util.SponsorBlockHelper; import org.schabi.newpipe.util.ThemeHelper; import java.util.List; @@ -229,9 +230,12 @@ public void onServiceConnected(final ComponentName name, final IBinder service) } else { onQueueUpdate(player.getPlayQueue()); buildComponents(); - if (player != null) { - player.setActivityListener(PlayQueueActivity.this); - } + player.setActivityListener(PlayQueueActivity.this); + player.getCurrentStreamInfo().ifPresent(info -> + SponsorBlockHelper.markSegments( + getApplicationContext(), + queueControlBinding.seekBar, + info)); } } }; 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 49e72328e4..4f0d2490dd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -57,6 +57,7 @@ import android.media.AudioManager; import android.util.Log; import android.view.LayoutInflater; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -69,6 +70,7 @@ import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.PositionInfo; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.MediaSource; @@ -86,6 +88,8 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -118,6 +122,9 @@ import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.SponsorBlockMode; +import org.schabi.newpipe.util.SponsorBlockSecondaryMode; +import org.schabi.newpipe.util.SponsorBlockHelper; import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.StreamTypeUtil; @@ -168,6 +175,7 @@ public final class Player implements PlaybackListener, Listener { public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 1000; // 1 second + private static final int UNSKIP_WINDOW_MILLIS = 5000; // 5 seconds /*////////////////////////////////////////////////////////////////////////// // Other constants @@ -262,7 +270,11 @@ public final class Player implements PlaybackListener, Listener { private final SharedPreferences prefs; @NonNull private final HistoryRecordManager recordManager; + private SponsorBlockMode sponsorBlockMode = SponsorBlockMode.DISABLED; + private SponsorBlockSegment lastSegment; + private boolean autoSkipGracePeriod = false; + private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; /*////////////////////////////////////////////////////////////////////////// // Constructor @@ -273,6 +285,25 @@ public Player(@NonNull final PlayerService service) { this.service = service; context = service; prefs = PreferenceManager.getDefaultSharedPreferences(context); + + final boolean isSponsorBlockEnabled = prefs.getBoolean( + context.getString(R.string.sponsor_block_enable_key), false); + + setSponsorBlockMode(isSponsorBlockEnabled + ? SponsorBlockMode.ENABLED + : SponsorBlockMode.DISABLED); + + preferenceChangeListener = + (sharedPreferences, key) -> { + if (context.getString(R.string.sponsor_block_enable_key).equals(key)) { + setSponsorBlockMode(sharedPreferences.getBoolean(key, false) + ? SponsorBlockMode.ENABLED + : SponsorBlockMode.DISABLED); + } + }; + + prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + recordManager = new HistoryRecordManager(context); setupBroadcastReceiver(); @@ -637,7 +668,7 @@ public void reloadPlayQueueManager() { } if (playQueue != null) { - playQueueManager = new MediaSourceManager(this, playQueue); + playQueueManager = new MediaSourceManager(context, this, playQueue); } } @@ -923,12 +954,130 @@ public boolean isProgressLoopRunning() { } public void triggerProgressUpdate() { + triggerProgressUpdate(false, false, false, false); + } + + public void triggerProgressUpdate(final boolean isRewind) { + triggerProgressUpdate(isRewind, false, false, false); + } + + private void triggerProgressUpdate(final boolean isRewind, + final boolean isGracedRewind, + final boolean bypassSecondaryMode, + final boolean isUnSkip) { if (exoPlayerIsNull()) { return; } - onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), - (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); + final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0); + + onUpdateProgress( + currentProgress, + (int) simpleExoPlayer.getDuration(), + simpleExoPlayer.getBufferedPercentage()); + + triggerCheckForSponsorBlockSegments(currentProgress, isRewind, + isGracedRewind, bypassSecondaryMode, isUnSkip); + } + + private void triggerCheckForSponsorBlockSegments(final int currentProgress, + final boolean isRewind, + final boolean isGracedRewind, + final boolean bypassSecondaryMode, + final boolean isUnSkip) { + if (sponsorBlockMode != SponsorBlockMode.ENABLED || !isPrepared) { + return; + } + + getSkippableSponsorBlockSegment(currentProgress).ifPresent(sponsorBlockSegment -> { + + final boolean showManualButtons = prefs.getBoolean( + context.getString(R.string.sponsor_block_show_manual_skip_key), false); + // per-sponsorBlockSegment category skip setting + final SponsorBlockSecondaryMode secondaryMode = getSecondaryMode(sponsorBlockSegment); + + // show/hide manual skip buttons + if (showManualButtons && secondaryMode != SponsorBlockSecondaryMode.HIGHLIGHT) { + if (currentProgress < sponsorBlockSegment.endTime + && currentProgress > sponsorBlockSegment.startTime) { + UIs.call(PlayerUi::showAutoSkip); + } else { + UIs.call(PlayerUi::hideAutoSkip); + } + + if (currentProgress > sponsorBlockSegment.startTime + && currentProgress < sponsorBlockSegment.endTime + UNSKIP_WINDOW_MILLIS) { + UIs.call(PlayerUi::showAutoUnskip); + } else { + UIs.call(PlayerUi::hideAutoUnskip); + } + } + + if (DEBUG) { + Log.d("SPONSOR_BLOCK", "Un-skip grace: isGracedRewind = " + + isGracedRewind + ", autoSkipGracePeriod = " + autoSkipGracePeriod); + } + + // temporarily pause auto skipping + // bypass grace when this is an un-skip request + if (!isGracedRewind) { + if (autoSkipGracePeriod) { + return; + } + } else { + + autoSkipGracePeriod = true; + } + + // prevent skip looping in unship window + if (lastSegment == sponsorBlockSegment && !bypassSecondaryMode) { + return; + } + + // Do not skip if highlight mode. Do not skip if manual mode + no explicit bypass + if (secondaryMode == SponsorBlockSecondaryMode.HIGHLIGHT + || (secondaryMode == SponsorBlockSecondaryMode.MANUAL + && !bypassSecondaryMode)) { + return; + } + + int skipTarget = isRewind + ? (int) Math.ceil((sponsorBlockSegment.startTime)) - 1 + : (int) Math.ceil((sponsorBlockSegment.endTime)); + + if (skipTarget < 0) { + skipTarget = 0; + } + + // temporarily force EXACT seek parameters to prevent infinite skip looping + final SeekParameters seekParams = simpleExoPlayer.getSeekParameters(); + simpleExoPlayer.setSeekParameters(SeekParameters.EXACT); + + seekTo(skipTarget); + + simpleExoPlayer.setSeekParameters(seekParams); + lastSegment = sponsorBlockSegment; + + if (isUnSkip) { + return; + } + + final boolean canShowNotifications = prefs.getBoolean( + context.getString(R.string.sponsor_block_notifications_key), false); + + if (canShowNotifications) { + final String toastText = + SponsorBlockHelper.convertCategoryToSkipMessage( + context, sponsorBlockSegment.category); + + Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show(); + } + + if (DEBUG) { + Log.d("SPONSOR_BLOCK", "Skipped segment: currentProgress = [" + + currentProgress + "], skipped to = [" + skipTarget + "]"); + } + }); } private Disposable getProgressUpdateDisposable() { @@ -1212,6 +1361,15 @@ public void toggleShuffleModeEnabled() { simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); } } + + public void toggleUnskip() { + triggerProgressUpdate(true, true, true, true); + } + + public void toggleSkip() { + autoSkipGracePeriod = false; + triggerProgressUpdate(false, true, true, false); + } //endregion @@ -1706,7 +1864,7 @@ public void playNext() { public void fastForward() { if (DEBUG) { - Log.d(TAG, "fastRewind() called"); + Log.d(TAG, "fastForward() called"); } seekBy(retrieveSeekDurationFromPreferences(this)); triggerProgressUpdate(); @@ -1717,7 +1875,13 @@ public void fastRewind() { Log.d(TAG, "fastRewind() called"); } seekBy(-retrieveSeekDurationFromPreferences(this)); - triggerProgressUpdate(); + if (prefs.getBoolean( + context.getString(R.string.sponsor_block_graced_rewind_key), false)) { + triggerProgressUpdate(true, true, false, false); + return; + } + + triggerProgressUpdate(true); } //endregion @@ -2302,6 +2466,121 @@ public Optional getFragmentListener() { return Optional.ofNullable(fragmentListener); } + public SponsorBlockMode getSponsorBlockMode() { + return sponsorBlockMode; + } + + public void setSponsorBlockMode(final SponsorBlockMode mode) { + sponsorBlockMode = mode; + } + + public Optional getSkippableSponsorBlockSegment(final int progress) { + return getCurrentStreamInfo().map(info -> { + final SponsorBlockSegment[] sponsorBlockSegments = info.getSponsorBlockSegments(); + if (sponsorBlockSegments == null) { + return null; + } + + for (final SponsorBlockSegment sponsorBlockSegment : sponsorBlockSegments) { + if (sponsorBlockSegment.action != SponsorBlockAction.SKIP) { + continue; + } + + if (progress < sponsorBlockSegment.startTime) { + continue; + } + + if (progress > sponsorBlockSegment.endTime) { + continue; + } + + return sponsorBlockSegment; + } + + // fallback on old SponsorBlockSegment (for un-skip) + if (lastSegment != null + && progress > lastSegment.endTime + UNSKIP_WINDOW_MILLIS) { + // un-skip window is over + hideUnskipButtons(); + lastSegment = null; + autoSkipGracePeriod = false; + + if (DEBUG) { + Log.d("SPONSOR_BLOCK", "Destroyed last segment variables (UNSKIP)"); + } + } else if (lastSegment != null + && progress < lastSegment.endTime + UNSKIP_WINDOW_MILLIS + && progress >= lastSegment.startTime) { + // use old sponsorBlockSegment if exists AND currentProgress in bounds + return lastSegment; + } + + hideUnskipButtons(); + return null; + }); + } + + private void hideUnskipButtons() { + if (DEBUG) { + Log.d("SPONSOR_BLOCK", "Hiding manual skip buttons (UNSKIP)"); + } + UIs.call(PlayerUi::hideAutoSkip); + UIs.call(PlayerUi::hideAutoUnskip); + } + + private SponsorBlockSecondaryMode getSecondaryMode(final SponsorBlockSegment segment) { + if (segment == null) { + return SponsorBlockSecondaryMode.DISABLED; + } + + // get pref + final String defaultValue = context.getString(R.string.sponsor_block_skip_mode_enabled); + final String key = switch (segment.category) { + case SPONSOR -> prefs.getString( + context.getString(R.string.sponsor_block_category_sponsor_mode_key), + defaultValue); + case INTRO -> prefs.getString( + context.getString(R.string.sponsor_block_category_intro_mode_key), + defaultValue); + case OUTRO -> prefs.getString( + context.getString(R.string.sponsor_block_category_outro_mode_key), + defaultValue); + case INTERACTION -> prefs.getString( + context.getString(R.string.sponsor_block_category_interaction_mode_key), + defaultValue); + case HIGHLIGHT -> "Highlight Only"; // not a regular "skippable" segment + case SELF_PROMO -> prefs.getString( + context.getString(R.string.sponsor_block_category_self_promo_mode_key), + defaultValue); + case NON_MUSIC -> prefs.getString( + context.getString(R.string.sponsor_block_category_non_music_mode_key), + defaultValue); + case PREVIEW -> prefs.getString( + context.getString(R.string.sponsor_block_category_preview_mode_key), + defaultValue); + case FILLER -> prefs.getString( + context.getString(R.string.sponsor_block_category_filler_mode_key), + defaultValue); + default -> ""; + }; + + // map pref to enum + final SponsorBlockSecondaryMode pref = + switch (key) { + case "Automatic" -> SponsorBlockSecondaryMode.ENABLED; + case "Manual" -> SponsorBlockSecondaryMode.MANUAL; + case "Highlight Only" -> SponsorBlockSecondaryMode.HIGHLIGHT; + default -> SponsorBlockSecondaryMode.DISABLED; + }; + + if (DEBUG) { + Log.d("SPONSOR_BLOCK", "Sponsor segment secondary mode: category = [" + + segment.category + "], preference = [" + pref + "]"); + } + + return pref; + } + /** * @return the user interfaces connected with the player */ diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerListener.java b/app/src/main/java/org/schabi/newpipe/player/PlayerListener.java new file mode 100644 index 0000000000..c7998b8b94 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerListener.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.player; + +public interface PlayerListener { + void onPlayerPrepared(Player player); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java index 30420b0c7d..154395c17a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java @@ -112,7 +112,7 @@ private synchronized NotificationCompat.Builder createNotification() { .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_TRANSPORT) .setShowWhen(false) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setColor(ContextCompat.getColor(player.getContext(), R.color.dark_background_color)) .setColorized(player.getPrefs().getBoolean( diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 88d7145bce..dbe103cfab 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -1,5 +1,11 @@ package org.schabi.newpipe.player.playback; +import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; +import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException; +import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG; +import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis; + +import android.content.Context; import android.os.Handler; import android.util.Log; @@ -38,11 +44,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject; -import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; -import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException; -import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG; -import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis; - public class MediaSourceManager { @NonNull private final String TAG = "MediaSourceManager@" + hashCode(); @@ -69,6 +70,8 @@ public class MediaSourceManager { */ private static final int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; + @NonNull + private final Context context; @NonNull private final PlaybackListener playbackListener; @NonNull @@ -125,14 +128,16 @@ public class MediaSourceManager { private final Handler removeMediaSourceHandler = new Handler(); - public MediaSourceManager(@NonNull final PlaybackListener listener, + public MediaSourceManager(@NonNull final Context context, + @NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, 400L, + this(context, listener, playQueue, 400L, /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS), /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS)); } - private MediaSourceManager(@NonNull final PlaybackListener listener, + private MediaSourceManager(@NonNull final Context context, + @NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue, final long loadDebounceMillis, final long playbackNearEndGapMillis, @@ -146,6 +151,7 @@ private MediaSourceManager(@NonNull final PlaybackListener listener, + " ms] for them to be useful."); } + this.context = context; this.playbackListener = listener; this.playQueue = playQueue; @@ -420,7 +426,7 @@ private void maybeLoadItem(@NonNull final PlayQueueItem item) { } private Single getLoadedMediaSource(@NonNull final PlayQueueItem stream) { - return stream.getStream() + return stream.getStream(context) .map(streamInfo -> Optional .ofNullable(playbackListener.sourceOf(stream, streamInfo)) .flatMap(source -> diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java index 759c512671..0e1d58cd0c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.player.playqueue; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -122,8 +124,8 @@ public Throwable getError() { } @NonNull - public Single getStream() { - return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false) + public Single getStream(final Context context) { + return ExtractorHelper.getStreamInfo(context, this.serviceId, this.url, false) .subscribeOn(Schedulers.io()) .doOnError(throwable -> error = throwable); } 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 index 57e2ec2a2c..3e1a1af3ff 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java @@ -127,6 +127,9 @@ public void onUpdateProgress(final int currentProgress, public void onPrepared() { } + public void onMarkSeekbarRequested(@NonNull final StreamInfo streamInfo) { + } + public void onBlocked() { } @@ -209,4 +212,28 @@ public void onPlayQueueEdited() { */ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { } + + /** + * Show SponsorBlock segment un-skip button. + */ + public void showAutoUnskip() { + } + + /** + * Hide SponsorBlock segment un-skip button. + */ + public void hideAutoUnskip() { + } + + /** + * Show SponsorBlock segment skip button. + */ + public void showAutoSkip() { + } + + /** + * Hide SponsorBlock segment skip button. + */ + public void hideAutoSkip() { + } } 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 index b51aaa6382..9e8bd977e5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -82,6 +82,7 @@ import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.SponsorBlockHelper; import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; @@ -215,6 +216,8 @@ protected void initListeners() { binding.repeatButton.setOnClickListener(v -> onRepeatClicked()); binding.shuffleButton.setOnClickListener(v -> onShuffleClicked()); + binding.unskipButton.setOnClickListener(v -> onUnskipClicked()); + binding.skipButton.setOnClickListener(v -> onSkipClicked()); binding.playPauseButton.setOnClickListener(makeOnClickListener(player::playPause)); binding.playPreviousButton.setOnClickListener(makeOnClickListener(player::playPrevious)); @@ -291,6 +294,8 @@ protected void deinitListeners() { binding.repeatButton.setOnClickListener(null); binding.shuffleButton.setOnClickListener(null); + binding.unskipButton.setOnClickListener(null); + binding.skipButton.setOnClickListener(null); binding.playPauseButton.setOnClickListener(null); binding.playPreviousButton.setOnClickListener(null); @@ -797,6 +802,11 @@ public void onPrepared() { binding.playbackSpeed.setText(formatSpeed(player.getPlaybackSpeed())); } + @Override + public void onMarkSeekbarRequested(@NonNull final StreamInfo streamInfo) { + SponsorBlockHelper.markSegments(context, binding.playbackSeekBar, streamInfo); + } + @Override public void onBlocked() { super.onBlocked(); @@ -851,6 +861,21 @@ public void onBuffering() { binding.getRoot().setKeepScreenOn(true); } + public void showAutoUnskip() { + binding.unskipButton.setVisibility(View.VISIBLE); + } + + public void hideAutoUnskip() { + binding.unskipButton.setVisibility(View.GONE); + } + + public void showAutoSkip() { + binding.skipButton.setVisibility(View.VISIBLE); + } + public void hideAutoSkip() { + binding.skipButton.setVisibility(View.GONE); + } + @Override public void onPaused() { super.onPaused(); @@ -947,6 +972,20 @@ public void onShuffleClicked() { player.toggleShuffleModeEnabled(); } + public void onUnskipClicked() { + if (DEBUG) { + Log.d(TAG, "onUnskipClicked() called"); + } + player.toggleUnskip(); + } + + public void onSkipClicked() { + if (DEBUG) { + Log.d(TAG, "onSkipClicked() called"); + } + player.toggleSkip(); + } + @Override public void onRepeatModeChanged(@RepeatMode final int repeatMode) { super.onRepeatModeChanged(repeatMode); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 32e33d55bf..a3b131d75f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -9,7 +9,6 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.ReleaseVersionUtil; public class MainSettingsFragment extends BasePreferenceFragment { public static final boolean DEBUG = MainActivity.DEBUG; @@ -22,14 +21,6 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called - // Check if the app is updatable - if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) { - getPreferenceScreen().removePreference( - findPreference(getString(R.string.update_pref_screen_key))); - - defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); - } - // Hide debug preferences in RELEASE build variant if (!DEBUG) { getPreferenceScreen().removePreference( 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 421440ea7f..d1a0b203ee 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -59,6 +59,9 @@ public static void initSettings(final Context context) { PreferenceManager.setDefaultValues(context, R.xml.update_settings, true); PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true); PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.sponsor_block_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.sponsor_block_category_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.return_youtube_dislikes_settings, true); saveDefaultVideoDownloadDirectory(context); saveDefaultAudioDownloadDirectory(context); @@ -98,7 +101,7 @@ public static File getDir(final String defaultDirectoryName) { } private static String getNewPipeChildFolderPathForDir(final File dir) { - return new File(dir, "NewPipe").toURI().toString(); + return new File(dir, "Tubular").toURI().toString(); } public static boolean useStorageAccessFramework(final Context context) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ReturnYouTubeDislikeSettingsFragment.java new file mode 100644 index 0000000000..1b119d8494 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/ReturnYouTubeDislikeSettingsFragment.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.settings; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import androidx.preference.Preference; + +import org.schabi.newpipe.R; + +public class ReturnYouTubeDislikeSettingsFragment extends BasePreferenceFragment { + + @Override + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { + addPreferencesFromResourceRegistry(); + + final Preference rydWebsitePreference = + findPreference(getString(R.string.return_youtube_dislike_home_page_key)); + rydWebsitePreference.setOnPreferenceClickListener((Preference p) -> { + final Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse(getString(R.string.return_youtube_dislike_home_page_url))); + startActivity(i); + return true; + }); + + final Preference rydSecurityFaqPreference = + findPreference(getString(R.string.return_youtube_dislike_security_faq_key)); + rydSecurityFaqPreference.setOnPreferenceClickListener((Preference p) -> { + final Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse(getString(R.string.return_youtube_dislike_security_faq_url))); + startActivity(i); + return true; + }); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 529e534422..00ee284ab4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -35,7 +35,6 @@ import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.KeyboardUtil; -import org.schabi.newpipe.util.ReleaseVersionUtil; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -265,13 +264,6 @@ private void initSearch( * be found when searching inside a release. */ private void ensureSearchRepresentsApplicationState() { - // Check if the update settings are available - if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) { - SettingsResourceRegistry.getInstance() - .getEntryByPreferencesResId(R.xml.update_settings) - .setSearchable(false); - } - // Hide debug preferences in RELEASE build variant if (DEBUG) { SettingsResourceRegistry.getInstance() diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index 06e0a7c1ea..0bf263d373 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -42,6 +42,9 @@ private SettingsResourceRegistry() { add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings); add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings); add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings); + add(SponsorBlockSettingsFragment.class, R.xml.sponsor_block_settings); + add(SponsorBlockCategoriesSettingsFragment.class, R.xml.sponsor_block_category_settings); + add(ReturnYouTubeDislikeSettingsFragment.class, R.xml.return_youtube_dislikes_settings); } private SettingRegistryEntry add( diff --git a/app/src/main/java/org/schabi/newpipe/settings/SponsorBlockCategoriesSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SponsorBlockCategoriesSettingsFragment.java new file mode 100644 index 0000000000..20cd7ceb42 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SponsorBlockCategoriesSettingsFragment.java @@ -0,0 +1,154 @@ +package org.schabi.newpipe.settings; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.annotation.ColorRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.settings.custom.EditColorPreference; + +public class SponsorBlockCategoriesSettingsFragment extends BasePreferenceFragment { + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { + addPreferencesFromResourceRegistry(); + + final Preference allOnPreference = + findPreference(getString(R.string.sponsor_block_category_all_on_key)); + allOnPreference.setOnPreferenceClickListener(p -> { + final SwitchPreference sponsorCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_sponsor_key)); + final SwitchPreference introCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_intro_key)); + final SwitchPreference outroCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_outro_key)); + final SwitchPreference interactionCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_interaction_key)); + final SwitchPreference highlightCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_highlight_key)); + final SwitchPreference selfPromoCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_self_promo_key)); + final SwitchPreference nonMusicCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_non_music_key)); + final SwitchPreference previewCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_preview_key)); + final SwitchPreference fillerCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_filler_key)); + + sponsorCategoryPreference.setChecked(true); + introCategoryPreference.setChecked(true); + outroCategoryPreference.setChecked(true); + interactionCategoryPreference.setChecked(true); + highlightCategoryPreference.setChecked(true); + selfPromoCategoryPreference.setChecked(true); + nonMusicCategoryPreference.setChecked(true); + previewCategoryPreference.setChecked(true); + fillerCategoryPreference.setChecked(true); + + return true; + }); + + final Preference allOffPreference = + findPreference(getString(R.string.sponsor_block_category_all_off_key)); + allOffPreference.setOnPreferenceClickListener(p -> { + final SwitchPreference sponsorCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_sponsor_key)); + final SwitchPreference introCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_intro_key)); + final SwitchPreference outroCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_outro_key)); + final SwitchPreference interactionCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_interaction_key)); + final SwitchPreference highlightCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_highlight_key)); + final SwitchPreference selfPromoCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_self_promo_key)); + final SwitchPreference nonMusicCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_non_music_key)); + final SwitchPreference previewCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_preview_key)); + final SwitchPreference fillerCategoryPreference = + findPreference(getString(R.string.sponsor_block_category_filler_key)); + + sponsorCategoryPreference.setChecked(false); + introCategoryPreference.setChecked(false); + outroCategoryPreference.setChecked(false); + interactionCategoryPreference.setChecked(false); + highlightCategoryPreference.setChecked(false); + selfPromoCategoryPreference.setChecked(false); + nonMusicCategoryPreference.setChecked(false); + previewCategoryPreference.setChecked(false); + fillerCategoryPreference.setChecked(false); + + return true; + }); + + final Preference resetPreference = + findPreference(getString(R.string.sponsor_block_category_reset_key)); + resetPreference.setOnPreferenceClickListener(p -> { + new AlertDialog.Builder(p.getContext()) + .setMessage(R.string.sponsor_block_confirm_reset_colors) + .setPositiveButton(R.string.yes, (dialog, which) -> { + final SharedPreferences.Editor editor = + getPreferenceManager() + .getSharedPreferences() + .edit(); + + setColorPreference(editor, + R.string.sponsor_block_category_sponsor_color_key, + R.color.sponsor_segment); + setColorPreference(editor, + R.string.sponsor_block_category_intro_color_key, + R.color.intro_segment); + setColorPreference(editor, + R.string.sponsor_block_category_outro_color_key, + R.color.outro_segment); + setColorPreference(editor, + R.string.sponsor_block_category_interaction_color_key, + R.color.interaction_segment); + setColorPreference(editor, + R.string.sponsor_block_category_highlight_color_key, + R.color.highlight_segment); + setColorPreference(editor, + R.string.sponsor_block_category_self_promo_color_key, + R.color.self_promo_segment); + setColorPreference(editor, + R.string.sponsor_block_category_non_music_color_key, + R.color.non_music_segment); + setColorPreference(editor, + R.string.sponsor_block_category_preview_color_key, + R.color.preview_segment); + setColorPreference(editor, + R.string.sponsor_block_category_filler_color_key, + R.color.filler_segment); + setColorPreference(editor, + R.string.sponsor_block_category_pending_color_key, + R.color.pending_segment); + + editor.apply(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + return true; + }); + } + + private void setColorPreference(final SharedPreferences.Editor editor, + @StringRes final int resId, + @ColorRes final int colorId) { + final String colorStr = "#" + Integer.toHexString(getResources().getColor(colorId)); + editor.putString(getString(resId), colorStr); + final EditColorPreference colorPreference = findPreference(getString(resId)); + colorPreference.setText(colorStr); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SponsorBlockSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SponsorBlockSettingsFragment.java new file mode 100644 index 0000000000..024834cebf --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SponsorBlockSettingsFragment.java @@ -0,0 +1,86 @@ +package org.schabi.newpipe.settings; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.local.sponsorblock.SponsorBlockDataManager; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class SponsorBlockSettingsFragment extends BasePreferenceFragment { + private SponsorBlockDataManager sponsorBlockDataManager; + private Disposable workerClearWhitelist; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + sponsorBlockDataManager = new SponsorBlockDataManager(getContext()); + } + + @Override + public void onDetach() { + super.onDetach(); + if (workerClearWhitelist != null) { + workerClearWhitelist.dispose(); + } + } + + @Override + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { + addPreferencesFromResourceRegistry(); + + final Preference sponsorBlockWebsitePreference = + findPreference(getString(R.string.sponsor_block_home_page_key)); + assert sponsorBlockWebsitePreference != null; + sponsorBlockWebsitePreference.setOnPreferenceClickListener((Preference p) -> { + final Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse(getString(R.string.sponsor_block_homepage_url))); + startActivity(i); + return true; + }); + + final Preference sponsorBlockPrivacyPreference = + findPreference(getString(R.string.sponsor_block_privacy_key)); + assert sponsorBlockPrivacyPreference != null; + sponsorBlockPrivacyPreference.setOnPreferenceClickListener((Preference p) -> { + final Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse(getString(R.string.sponsor_block_privacy_policy_url))); + startActivity(i); + return true; + }); + + final Preference sponsorBlockClearWhitelistPreference = + findPreference(getString(R.string.sponsor_block_clear_whitelist_key)); + assert sponsorBlockClearWhitelistPreference != null; + sponsorBlockClearWhitelistPreference.setOnPreferenceClickListener((Preference p) -> { + new AlertDialog.Builder(p.getContext()) + .setMessage(R.string.sponsor_block_confirm_clear_whitelist) + .setPositiveButton(R.string.yes, (dialog, which) -> { + workerClearWhitelist = + sponsorBlockDataManager.clearWhitelist() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + Toast.makeText(p.getContext(), + R.string.sponsor_block_whitelist_cleared_toast, + Toast.LENGTH_SHORT).show(); + }, error -> { + // TODO + }); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + return true; + }); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/EditColorPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/EditColorPreference.java new file mode 100644 index 0000000000..633c52afc1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/EditColorPreference.java @@ -0,0 +1,81 @@ +package org.schabi.newpipe.settings.custom; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Toast; + +import androidx.preference.EditTextPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import org.schabi.newpipe.R; + +public class EditColorPreference extends EditTextPreference + implements Preference.OnPreferenceChangeListener { + private PreferenceViewHolder viewHolder; + + public EditColorPreference(final Context context, final AttributeSet attrs, + final int defStyleAttr, final int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public EditColorPreference(final Context context, final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public EditColorPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + init(); + } + + public EditColorPreference(final Context context) { + super(context); + init(); + } + + private void init() { + setWidgetLayoutResource(R.layout.preference_edit_color); + setOnPreferenceChangeListener(this); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + viewHolder = holder; + + final String colorStr = + getPreferenceManager() + .getSharedPreferences() + .getString(getKey(), null); + + if (colorStr == null) { + return; + } + + final int color = Color.parseColor(colorStr); + + final View view = viewHolder.findViewById(R.id.sponsor_block_segment_color_view); + view.setBackgroundColor(color); + } + + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + try { + final int color = Color.parseColor((String) newValue); + + final View view = viewHolder.findViewById(R.id.sponsor_block_segment_color_view); + view.setBackgroundColor(color); + + return true; + } catch (final Exception e) { + Toast.makeText(getContext(), R.string.invalid_color_toast, Toast.LENGTH_SHORT).show(); + return false; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/ReturnYouTubeDislikeApiUrlPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/ReturnYouTubeDislikeApiUrlPreference.java new file mode 100644 index 0000000000..a900ef10f4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/ReturnYouTubeDislikeApiUrlPreference.java @@ -0,0 +1,105 @@ +package org.schabi.newpipe.settings.custom; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.net.Uri; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import org.schabi.newpipe.R; + +public class ReturnYouTubeDislikeApiUrlPreference extends Preference { + public ReturnYouTubeDislikeApiUrlPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + @Override + protected void onSetInitialValue(@Nullable final Object defaultValue) { + // apparently this is how you're supposed to respect default values for a custom preference + persistString(getPersistedString((String) defaultValue)); + } + + @Nullable + @Override + protected Object onGetDefaultValue(@NonNull final TypedArray a, final int index) { + return a.getString(index); + } + + @Override + protected void onClick() { + super.onClick(); + + final Context context = getContext(); + + final String apiUrl = getPersistedString(null); + + final View alertDialogView = LayoutInflater.from(context) + .inflate(R.layout.dialog_return_youtube_dislike_api_url, null); + + final EditText editText = alertDialogView.findViewById(R.id.api_url_edit); + editText.setText(apiUrl); + editText.setOnFocusChangeListener((v, hasFocus) -> editText.post(() -> { + final InputMethodManager inputMethodManager = (InputMethodManager) context + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager + .showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); + })); + editText.requestFocus(); + + alertDialogView.findViewById(R.id.icon_api_url_help) + .setOnClickListener(v -> { + final Uri privacyPolicyUri = Uri.parse(context + .getString(R.string.return_youtube_dislike_security_faq_url)); + final View helpDialogView = LayoutInflater.from(context) + .inflate( + R.layout.dialog_return_youtube_dislike_api_url_help, null); + final View privacyPolicyButton = helpDialogView + .findViewById(R.id.return_youtube_dislike_security_faq_button); + privacyPolicyButton.setOnClickListener(v1 -> { + final Intent i = new Intent(Intent.ACTION_VIEW, privacyPolicyUri); + context.startActivity(i); + }); + + new AlertDialog.Builder(context) + .setView(helpDialogView) + .setPositiveButton("Use Official", (dialog, which) -> { + editText.setText(context.getString( + R.string.return_youtube_dislike_default_api_url)); + dialog.dismiss(); + }) + .setNeutralButton("Close", (dialog, which) -> dialog.dismiss()) + .create() + .show(); + }); + + final AlertDialog alertDialog = + new AlertDialog.Builder(context) + .setView(alertDialogView) + .setTitle(context.getString(R.string.return_youtube_dislike_api_url_title)) + .setPositiveButton("OK", (dialog, which) -> { + final String newValue = editText.getText().toString(); + if (!newValue.isEmpty()) { + final SharedPreferences.Editor editor = + getPreferenceManager().getSharedPreferences().edit(); + editor.putString(getKey(), newValue); + editor.apply(); + + callChangeListener(newValue); + } + dialog.dismiss(); + }) + .setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()) + .create(); + + alertDialog.show(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/SponsorBlockApiUrlPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/SponsorBlockApiUrlPreference.java new file mode 100644 index 0000000000..1a89405844 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/SponsorBlockApiUrlPreference.java @@ -0,0 +1,104 @@ +package org.schabi.newpipe.settings.custom; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.net.Uri; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import org.schabi.newpipe.R; + +public class SponsorBlockApiUrlPreference extends Preference { + public SponsorBlockApiUrlPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + @Override + protected void onSetInitialValue(@Nullable final Object defaultValue) { + // apparently this is how you're supposed to respect default values for a custom preference + persistString(getPersistedString((String) defaultValue)); + } + + @Nullable + @Override + protected Object onGetDefaultValue(@NonNull final TypedArray a, final int index) { + return a.getString(index); + } + + @Override + protected void onClick() { + super.onClick(); + + final Context context = getContext(); + + final String apiUrl = getPersistedString(null); + + final View alertDialogView = LayoutInflater.from(context) + .inflate(R.layout.dialog_sponsor_block_api_url, null); + + final EditText editText = alertDialogView.findViewById(R.id.api_url_edit); + editText.setText(apiUrl); + editText.setOnFocusChangeListener((v, hasFocus) -> editText.post(() -> { + final InputMethodManager inputMethodManager = (InputMethodManager) context + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager + .showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); + })); + editText.requestFocus(); + + alertDialogView.findViewById(R.id.icon_api_url_help) + .setOnClickListener(v -> { + final Uri privacyPolicyUri = Uri.parse(context + .getString(R.string.sponsor_block_privacy_policy_url)); + final View helpDialogView = LayoutInflater.from(context) + .inflate(R.layout.dialog_sponsor_block_api_url_help, null); + final View privacyPolicyButton = helpDialogView + .findViewById(R.id.sponsor_block_privacy_policy_button); + privacyPolicyButton.setOnClickListener(v1 -> { + final Intent i = new Intent(Intent.ACTION_VIEW, privacyPolicyUri); + context.startActivity(i); + }); + + new AlertDialog.Builder(context) + .setView(helpDialogView) + .setPositiveButton("Use Official", (dialog, which) -> { + editText.setText(context + .getString(R.string.sponsor_block_default_api_url)); + dialog.dismiss(); + }) + .setNeutralButton("Close", (dialog, which) -> dialog.dismiss()) + .create() + .show(); + }); + + final AlertDialog alertDialog = + new AlertDialog.Builder(context) + .setView(alertDialogView) + .setTitle(context.getString(R.string.sponsor_block_api_url_title)) + .setPositiveButton("OK", (dialog, which) -> { + final String newValue = editText.getText().toString(); + if (!newValue.isEmpty()) { + final SharedPreferences.Editor editor = + getPreferenceManager().getSharedPreferences().edit(); + editor.putString(getKey(), newValue); + editor.apply(); + + callChangeListener(newValue); + } + dialog.dismiss(); + }) + .setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()) + .create(); + + alertDialog.show(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index 7e3f5d0c82..aeb5d5860b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -191,7 +191,7 @@ public int getTabId() { public String getTabName(final Context context) { // TODO: find a better name for the blank tab (maybe "blank_tab") or replace it with // context.getString(R.string.app_name); - return "NewPipe"; // context.getString(R.string.blank_page_summary); + return "Tubular"; // context.getString(R.string.blank_page_summary); } @DrawableRes diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 066d5f5704..0d11622212 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -23,6 +23,7 @@ import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD; import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import android.view.View; import android.widget.TextView; @@ -47,7 +48,9 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.returnyoutubedislike.ReturnYouTubeDislikeApiSettings; import org.schabi.newpipe.extractor.search.SearchInfo; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockApiSettings; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; @@ -111,14 +114,40 @@ public static Single> suggestionsFor(final int serviceId, final Str }); } - public static Single getStreamInfo(final int serviceId, final String url, + public static Single getStreamInfo(final Context context, + final int serviceId, + final String url, final boolean forceLoad) { checkServiceId(serviceId); return checkCache(forceLoad, serviceId, url, InfoCache.Type.STREAM, - Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url))); + Single.fromCallable(() -> StreamInfo.getInfo( + NewPipe.getService(serviceId), + url, + buildSponsorBlockApiSettings(context), + buildReturnYouTubeDislikeApiSettings(context)))); } - public static Single getChannelInfo(final int serviceId, final String url, + private static ReturnYouTubeDislikeApiSettings buildReturnYouTubeDislikeApiSettings( + final Context context) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + final boolean isRydEnabled = prefs.getBoolean(context + .getString(R.string.return_youtube_dislike_enable_key), false); + + if (!isRydEnabled) { + return null; + } + + final ReturnYouTubeDislikeApiSettings result = new ReturnYouTubeDislikeApiSettings(); + result.apiUrl = + prefs.getString( + context.getString(R.string.return_youtube_dislike_api_url_key), null); + + return result; + } + + public static Single getChannelInfo(final int serviceId, + final String url, final boolean forceLoad) { checkServiceId(serviceId); return checkCache(forceLoad, serviceId, url, InfoCache.Type.CHANNEL, @@ -355,4 +384,49 @@ private static String capitalizeIfAllUppercase(final String text) { return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase(); } } + + private static @Nullable SponsorBlockApiSettings buildSponsorBlockApiSettings( + final Context context) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + final boolean isSponsorBlockEnabled = prefs.getBoolean(context + .getString(R.string.sponsor_block_enable_key), false); + + if (!isSponsorBlockEnabled) { + return null; + } + + final SponsorBlockApiSettings result = new SponsorBlockApiSettings(); + result.apiUrl = + prefs.getString(context.getString(R.string.sponsor_block_api_url_key), null); + result.includeSponsorCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_sponsor_key), false); + result.includeIntroCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_intro_key), false); + result.includeOutroCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_outro_key), false); + result.includeInteractionCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_interaction_key), false); + result.includeHighlightCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_highlight_key), false); + result.includeSelfPromoCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_self_promo_key), false); + result.includeMusicCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_non_music_key), false); + result.includePreviewCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_preview_key), false); + result.includeFillerCategory = + prefs.getBoolean(context + .getString(R.string.sponsor_block_category_filler_key), false); + + return result; + } } 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 bc113e8f86..b7cb69454c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -165,6 +165,10 @@ public static String localizeWatchingCount(@NonNull final Context context, localizeNumber(context, watchingCount)); } + public static String localizePercentage(final double number) { + return String.format(Locale.US, "%.0f%%", number); + } + public static String shortCount(@NonNull final Context context, final long count) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return CompactDecimalFormat.getInstance(getAppLocale(context), 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 6e9ea7a47e..5cccf35853 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java @@ -102,7 +102,7 @@ public static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context conte @NonNull final String url, final Consumer callback) { Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show(); - ExtractorHelper.getStreamInfo(serviceId, url, false) + ExtractorHelper.getStreamInfo(context, serviceId, url, false) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { diff --git a/app/src/main/java/org/schabi/newpipe/util/SponsorBlockHelper.java b/app/src/main/java/org/schabi/newpipe/util/SponsorBlockHelper.java new file mode 100644 index 0000000000..77b7ebf68c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SponsorBlockHelper.java @@ -0,0 +1,190 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory; +import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.views.MarkableSeekBar; +import org.schabi.newpipe.views.SeekBarMarker; + +public final class SponsorBlockHelper { + + + private SponsorBlockHelper() { + } + + public static Integer convertCategoryToColor( + final SponsorBlockCategory category, + final Context context + ) { + final String key; + final String colorStr; + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + switch (category) { + case SPONSOR -> { + key = context.getString(R.string.sponsor_block_category_sponsor_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.sponsor_segment) + : Color.parseColor(colorStr); + } + case INTRO -> { + key = context.getString(R.string.sponsor_block_category_intro_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.intro_segment) + : Color.parseColor(colorStr); + } + case OUTRO -> { + key = context.getString(R.string.sponsor_block_category_outro_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.outro_segment) + : Color.parseColor(colorStr); + } + case INTERACTION -> { + key = context.getString(R.string.sponsor_block_category_interaction_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.interaction_segment) + : Color.parseColor(colorStr); + } + case HIGHLIGHT -> { + key = context.getString(R.string.sponsor_block_category_highlight_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.highlight_segment) + : Color.parseColor(colorStr); + } + case SELF_PROMO -> { + key = context.getString(R.string.sponsor_block_category_self_promo_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.self_promo_segment) + : Color.parseColor(colorStr); + } + case NON_MUSIC -> { + key = context.getString(R.string.sponsor_block_category_non_music_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.non_music_segment) + : Color.parseColor(colorStr); + } + case PREVIEW -> { + key = context.getString(R.string.sponsor_block_category_preview_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.preview_segment) + : Color.parseColor(colorStr); + } + case FILLER -> { + key = context.getString(R.string.sponsor_block_category_filler_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.filler_segment) + : Color.parseColor(colorStr); + } + case PENDING -> { + key = context.getString(R.string.sponsor_block_category_pending_color_key); + colorStr = prefs.getString(key, null); + return colorStr == null + ? context.getResources().getColor(R.color.pending_segment) + : Color.parseColor(colorStr); + } + } + + return null; + } + + public static void markSegments( + final Context context, + final MarkableSeekBar seekBar, + @NonNull final StreamInfo streamInfo + ) { + seekBar.clearMarkers(); + + final SponsorBlockSegment[] sponsorBlockSegments = streamInfo.getSponsorBlockSegments(); + + if (sponsorBlockSegments == null) { + return; + } + + for (final SponsorBlockSegment sponsorBlockSegment : sponsorBlockSegments) { + final Integer color = convertCategoryToColor( + sponsorBlockSegment.category, context); + + // if null, then this category should not be marked + if (color == null) { + continue; + } + + // duration is in seconds, we need milliseconds + final long length = streamInfo.getDuration() * 1000; + + final SeekBarMarker seekBarMarker = + new SeekBarMarker(sponsorBlockSegment.startTime, sponsorBlockSegment.endTime, + length, color); + seekBar.seekBarMarkers.add(seekBarMarker); + } + + seekBar.drawMarkers(); + } + + public static String convertCategoryToFriendlyName(final Context context, + final SponsorBlockCategory category) { + return switch (category) { + case SPONSOR -> context.getString( + R.string.sponsor_block_category_sponsor); + case INTRO -> context.getString( + R.string.sponsor_block_category_intro); + case OUTRO -> context.getString( + R.string.sponsor_block_category_outro); + case INTERACTION -> context.getString( + R.string.sponsor_block_category_interaction); + case HIGHLIGHT -> context.getString( + R.string.sponsor_block_category_highlight); + case SELF_PROMO -> context.getString( + R.string.sponsor_block_category_self_promo); + case NON_MUSIC -> context.getString( + R.string.sponsor_block_category_non_music); + case PREVIEW -> context.getString( + R.string.sponsor_block_category_preview); + case FILLER -> context.getString( + R.string.sponsor_block_category_filler); + case PENDING -> context.getString( + R.string.sponsor_block_category_pending); + }; + } + + public static String convertCategoryToSkipMessage(final Context context, + final SponsorBlockCategory category) { + return switch (category) { + case SPONSOR -> context + .getString(R.string.sponsor_block_skip_sponsor_toast); + case INTRO -> context + .getString(R.string.sponsor_block_skip_intro_toast); + case OUTRO -> context + .getString(R.string.sponsor_block_skip_outro_toast); + case INTERACTION -> context + .getString(R.string.sponsor_block_skip_interaction_toast); + case HIGHLIGHT -> ""; // this should never happen + case SELF_PROMO -> context + .getString(R.string.sponsor_block_skip_self_promo_toast); + case NON_MUSIC -> context + .getString(R.string.sponsor_block_skip_non_music_toast); + case PREVIEW -> context + .getString(R.string.sponsor_block_skip_preview_toast); + case FILLER -> context + .getString(R.string.sponsor_block_skip_filler_toast); + case PENDING -> context + .getString(R.string.sponsor_block_skip_pending_toast); + }; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/SponsorBlockMode.java b/app/src/main/java/org/schabi/newpipe/util/SponsorBlockMode.java new file mode 100644 index 0000000000..395d6ad33f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SponsorBlockMode.java @@ -0,0 +1,6 @@ +package org.schabi.newpipe.util; + +public enum SponsorBlockMode { + DISABLED, + ENABLED +} diff --git a/app/src/main/java/org/schabi/newpipe/util/SponsorBlockSecondaryMode.java b/app/src/main/java/org/schabi/newpipe/util/SponsorBlockSecondaryMode.java new file mode 100644 index 0000000000..d6dd69b037 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SponsorBlockSecondaryMode.java @@ -0,0 +1,8 @@ +package org.schabi.newpipe.util; + +public enum SponsorBlockSecondaryMode { + DISABLED, + ENABLED, + MANUAL, + HIGHLIGHT +} diff --git a/app/src/main/java/org/schabi/newpipe/util/TimeUtils.java b/app/src/main/java/org/schabi/newpipe/util/TimeUtils.java new file mode 100644 index 0000000000..1a4d73b16e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/TimeUtils.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.util; + +import java.util.Locale; + +public final class TimeUtils { + private TimeUtils() { + } + + public static String millisecondsToString(final double milliseconds) { + final int seconds = (int) (milliseconds / 1000) % 60; + final int minutes = (int) ((milliseconds / (1000 * 60)) % 60); + final int hours = (int) ((milliseconds / (1000 * 60 * 60)) % 24); + + return String.format(Locale.getDefault(), + "%02d:%02d:%02d", hours, minutes, seconds); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/Version.java b/app/src/main/java/org/schabi/newpipe/util/Version.java new file mode 100644 index 0000000000..811aee1b74 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/Version.java @@ -0,0 +1,79 @@ +package org.schabi.newpipe.util; + +import android.util.Log; + +public final class Version implements Comparable { + private static final String TAG = Version.class.getSimpleName(); + private final int major; + private final int minor; + private final int build; + private final int rev; + + public Version(final int major, final int minor, final int build, final int rev) { + this.major = major; + this.minor = minor; + this.build = build; + this.rev = rev; + } + + public static Version fromString(final String str) { + // examples of valid version strings: + // - 0.1 + // - v0.1.0.4 + // - 0.20.6 + // - v0.20.6_r2 + try { + // example: v0.20.6_r2 -> v0.20.6.r2 -> 0.20.6.2 + final String[] split = str + .replaceAll("_", ".") + .replaceAll("[^0-9.]", "") + .split("[^\\d]"); + + final int major = Integer.parseInt(split[0]); + final int minor = split.length > 1 + ? Integer.parseInt(split[1]) + : 0; + final int build = split.length > 2 + ? Integer.parseInt(split[2]) + : 0; + final int rev = split.length > 3 + ? Integer.parseInt(split[3]) + : 0; + + return new Version(major, minor, build, rev); + } catch (final Exception e) { + Log.e(TAG, "Could not successfully parse version string.", e); + return new Version(0, 0, 0, 0); + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getBuild() { + return build; + } + + public int getRev() { + return rev; + } + + @Override + public int compareTo(final Version that) { + if (this.getMajor() != that.getMajor()) { + return this.getMajor() < that.getMajor() ? -1 : 1; + } else if (this.getMinor() != that.getMinor()) { + return this.getMinor() < that.getMinor() ? -1 : 1; + } else if (this.getBuild() != that.getBuild()) { + return this.getBuild() < that.getBuild() ? -1 : 1; + } else if (this.getRev() != that.getRev()) { + return this.getRev() < that.getRev() ? -1 : 1; + } + return 0; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java index 4b116bdf90..5a45ca933f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/image/PicassoHelper.java @@ -130,7 +130,7 @@ public static RequestCreator loadSeekbarThumbnailPreview(@Nullable final String } public static RequestCreator loadNotificationIcon(@Nullable final String url) { - return loadImageDefault(url, R.drawable.ic_newpipe_triangle_white); + return loadImageDefault(url, R.drawable.ic_tubular_white); } diff --git a/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java index 066515d6b9..322ae17a5c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java @@ -154,7 +154,7 @@ public static boolean playOnPopup(final Context context, } final Single single = - ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); + ExtractorHelper.getStreamInfo(context, service.getServiceId(), cleanUrl, false); disposables.add(single.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java index 8176a9aef7..2622d521ca 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -24,8 +24,6 @@ import android.view.ViewTreeObserver; import android.widget.SeekBar; -import androidx.appcompat.widget.AppCompatSeekBar; - import org.schabi.newpipe.util.DeviceUtils; /** @@ -33,7 +31,7 @@ * (onStartTrackingTouch/onStopTrackingTouch), so existing code does not need to be changed to * work with it. */ -public final class FocusAwareSeekBar extends AppCompatSeekBar { +public final class FocusAwareSeekBar extends MarkableSeekBar { private NestedListener listener; private ViewTreeObserver treeObserver; diff --git a/app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java new file mode 100644 index 0000000000..82dcda0c60 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java @@ -0,0 +1,109 @@ +package org.schabi.newpipe.views; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.util.AttributeSet; + +import androidx.appcompat.widget.AppCompatSeekBar; +import androidx.core.content.ContextCompat; + +import org.schabi.newpipe.R; + +import java.util.ArrayList; + +public class MarkableSeekBar extends AppCompatSeekBar { + public ArrayList seekBarMarkers = new ArrayList<>(); + private Drawable originalProgressDrawable; + + public MarkableSeekBar(final Context context) { + super(context); + } + + public MarkableSeekBar(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + public MarkableSeekBar(final Context context, + final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setProgressDrawable(final Drawable d) { + super.setProgressDrawable(d); + + // stored for when we draw (and potentially re-draw) markers + originalProgressDrawable = d; + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldW, final int oldH) { + super.onSizeChanged(w, h, oldW, oldH); + + // re-draw markers since the progress bar may have a different width + drawMarkers(); + } + + public void drawMarkers() { + if (seekBarMarkers.size() == 0) { + return; + } + + // Markers are drawn like so: + // + // - LayerDrawable (original drawable for the SeekBar) + // - GradientDrawable (background) + // - ScaleDrawable (secondaryProgress) + // - ScaleDrawable (progress) + // - LayerDrawable (we add our markers in a sub-LayerDrawable) + // - Drawable (marker) + // - Drawable (marker) + // - Drawable (marker) + // - etc... + + final int width = getMeasuredWidth() - (getPaddingStart() + getPaddingEnd()); + + LayerDrawable layerDrawable = (LayerDrawable) originalProgressDrawable; + + final ArrayList markerDrawables = new ArrayList<>(); + markerDrawables.add(layerDrawable); + + for (final SeekBarMarker seekBarMarker : seekBarMarkers) { + @SuppressLint("PrivateResource") + final Drawable markerDrawable = + ContextCompat.getDrawable( + getContext(), + R.drawable.abc_scrubber_primary_mtrl_alpha); + + final PorterDuffColorFilter colorFilter = + new PorterDuffColorFilter(seekBarMarker.color, PorterDuff.Mode.SRC_IN); + + assert markerDrawable != null; + markerDrawable.setColorFilter(colorFilter); + + markerDrawables.add(markerDrawable); + } + + layerDrawable = new LayerDrawable(markerDrawables.toArray(new Drawable[0])); + + for (int i = 1; i < layerDrawable.getNumberOfLayers(); i++) { + final SeekBarMarker seekBarMarker = seekBarMarkers.get(i - 1); + final int l = (int) (width * seekBarMarker.percentStart); + final int r = (int) (width * (1.0 - seekBarMarker.percentEnd)); + + layerDrawable.setLayerInset(i, l, 0, r, 0); + } + + super.setProgressDrawable(layerDrawable); + } + + public void clearMarkers() { + seekBarMarkers.clear(); + super.setProgressDrawable(originalProgressDrawable); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java b/app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java new file mode 100644 index 0000000000..22a8c5fa67 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java @@ -0,0 +1,26 @@ +package org.schabi.newpipe.views; + +public class SeekBarMarker { + public double startTime; + public double endTime; + public double percentStart; + public double percentEnd; + public int color; + + public SeekBarMarker(final double startTime, + final double endTime, + final long maxTime, + final int color) { + this.startTime = startTime; + this.endTime = endTime; + this.percentStart = ((startTime / maxTime) * 100.0) / 100.0; + this.percentEnd = ((endTime / maxTime) * 100.0) / 100.0; + this.color = color; + } + + public SeekBarMarker(final double percentStart, final double percentEnd, final int color) { + this.percentStart = percentStart; + this.percentEnd = percentEnd; + this.color = color; + } +} 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 31e7f663de..3f622b6668 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 @@ -670,7 +670,7 @@ private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem opt = new NotificationCompat.Builder(mContext, mContext.getString(R.string.hash_channel_id)) .setPriority(NotificationCompat.PRIORITY_HIGH) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_tubular_white) .setContentTitle(mContext.getString(R.string.msg_calculating_hash)) .setContentText(mContext.getString(R.string.msg_wait)) .setProgress(0, 0, true) diff --git a/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png deleted file mode 100644 index dd36385796..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_newpipe_update.png b/app/src/main/res/drawable-hdpi/ic_newpipe_update.png deleted file mode 100755 index 047d2f798f..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_newpipe_update.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_tubular_update.png b/app/src/main/res/drawable-hdpi/ic_tubular_update.png new file mode 100644 index 0000000000..d73986e20e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_tubular_update.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_tubular_white.png b/app/src/main/res/drawable-hdpi/ic_tubular_white.png new file mode 100644 index 0000000000..afa6e12278 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_tubular_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png deleted file mode 100644 index e5d102eda0..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_newpipe_update.png b/app/src/main/res/drawable-mdpi/ic_newpipe_update.png deleted file mode 100755 index bec3631ab1..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_newpipe_update.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_tubular_update.png b/app/src/main/res/drawable-mdpi/ic_tubular_update.png new file mode 100644 index 0000000000..e19d05347e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_tubular_update.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_tubular_white.png b/app/src/main/res/drawable-mdpi/ic_tubular_white.png new file mode 100644 index 0000000000..8447078c8c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_tubular_white.png differ diff --git a/app/src/main/res/drawable-night/splash_background.xml b/app/src/main/res/drawable-night/splash_background.xml index 237f4cdae2..59437bd162 100644 --- a/app/src/main/res/drawable-night/splash_background.xml +++ b/app/src/main/res/drawable-night/splash_background.xml @@ -4,9 +4,7 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png deleted file mode 100644 index a875fac86f..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png deleted file mode 100755 index 31eba305c9..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tubular_update.png b/app/src/main/res/drawable-xhdpi/ic_tubular_update.png new file mode 100644 index 0000000000..e167102168 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tubular_update.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tubular_white.png b/app/src/main/res/drawable-xhdpi/ic_tubular_white.png new file mode 100644 index 0000000000..61f9d75843 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tubular_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png deleted file mode 100644 index e6e661b415..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png deleted file mode 100755 index eda411234d..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tubular_update.png b/app/src/main/res/drawable-xxhdpi/ic_tubular_update.png new file mode 100644 index 0000000000..adf09c4359 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tubular_update.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tubular_white.png b/app/src/main/res/drawable-xxhdpi/ic_tubular_white.png new file mode 100644 index 0000000000..6842f85e96 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tubular_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png deleted file mode 100644 index 2185943990..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png deleted file mode 100755 index 0771140c1f..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tubular_update.png b/app/src/main/res/drawable-xxxhdpi/ic_tubular_update.png new file mode 100644 index 0000000000..adf09c4359 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tubular_update.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tubular_white.png b/app/src/main/res/drawable-xxxhdpi/ic_tubular_white.png new file mode 100644 index 0000000000..d4c5816a90 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tubular_white.png differ diff --git a/app/src/main/res/drawable/background_rectangle_black_transparent.xml b/app/src/main/res/drawable/background_rectangle_black_transparent.xml new file mode 100644 index 0000000000..e2db51a5c7 --- /dev/null +++ b/app/src/main/res/drawable/background_rectangle_black_transparent.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml new file mode 100644 index 0000000000..5d3e3f85fb --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 0000000000..909fdd16d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_forward.xml b/app/src/main/res/drawable/ic_fast_forward.xml new file mode 100644 index 0000000000..4edc96a9b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_forward.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_rewind.xml b/app/src/main/res/drawable/ic_fast_rewind.xml new file mode 100644 index 0000000000..33d9f56ef8 --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_rewind.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sponsor_block_disable.xml b/app/src/main/res/drawable/ic_sponsor_block_disable.xml new file mode 100644 index 0000000000..5965ef4c89 --- /dev/null +++ b/app/src/main/res/drawable/ic_sponsor_block_disable.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_sponsor_block_enable.xml b/app/src/main/res/drawable/ic_sponsor_block_enable.xml new file mode 100644 index 0000000000..7bd3d91cef --- /dev/null +++ b/app/src/main/res/drawable/ic_sponsor_block_enable.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_sponsor_block_exclude.xml b/app/src/main/res/drawable/ic_sponsor_block_exclude.xml new file mode 100644 index 0000000000..779a400f60 --- /dev/null +++ b/app/src/main/res/drawable/ic_sponsor_block_exclude.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_upload.xml b/app/src/main/res/drawable/ic_upload.xml new file mode 100644 index 0000000000..cf996d1972 --- /dev/null +++ b/app/src/main/res/drawable/ic_upload.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml index c9b018add4..a3ca03c1a9 100644 --- a/app/src/main/res/drawable/splash_background.xml +++ b/app/src/main/res/drawable/splash_background.xml @@ -4,9 +4,7 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_foreground.xml b/app/src/main/res/drawable/splash_foreground.xml index 63fd0351f2..e63b2f61ef 100644 --- a/app/src/main/res/drawable/splash_foreground.xml +++ b/app/src/main/res/drawable/splash_foreground.xml @@ -5,6 +5,16 @@ android:viewportHeight="100"> + android:pathData="M7.68,16.35 L7.84,83.65 64.11,50.08ZM18.56,35.08 L43.82,49.84 18.64,64.77z" + android:strokeWidth="4.93903" + android:strokeColor="#00000000" + android:strokeLineCap="butt" + android:strokeLineJoin="miter" /> + 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 a5df5e5662..ec00c01ad0 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 @@ -280,7 +280,7 @@ tools:ignore="HardcodedText" tools:text="1:06:29" /> - + + + + + + + + + + + + + + + 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 29efa36f92..87f216bb69 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -116,7 +116,7 @@ tools:text="1:06:29" /> - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_return_youtube_dislike_api_url_help.xml b/app/src/main/res/layout/dialog_return_youtube_dislike_api_url_help.xml new file mode 100644 index 0000000000..9e5f44cde4 --- /dev/null +++ b/app/src/main/res/layout/dialog_return_youtube_dislike_api_url_help.xml @@ -0,0 +1,25 @@ + + + +