diff --git a/.github/workflows/backport-pr.yml b/.github/workflows/backport-pr.yml new file mode 100644 index 000000000..c7bcb117e --- /dev/null +++ b/.github/workflows/backport-pr.yml @@ -0,0 +1,46 @@ +name: Backport merged pull request +on: + issue_comment: + types: [created] +permissions: + contents: write # for comment creation on original PR + pull-requests: write +jobs: + backport: + name: Backport pull request + runs-on: ubuntu-latest + + # Only run when the comment starts with the `/backport` command on a PR and + # the commenter has write access to the repository. We do not want to allow + # everybody to trigger backports and create branches in our repository. + if: > + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/backport ') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'COLLABORATOR' || + github.event.comment.author_association == 'MEMBER' + ) + steps: + - uses: actions/checkout@v4 + - name: Get backport metadata + # the target branch is the first argument after `/backport` + run: | + set -euo pipefail + body="${{ github.event.comment.body }}" + + line=${body%%$'\n'*} # Get the first line + if [[ $line =~ ^/backport[[:space:]]+([^[:space:]]+) ]]; then + echo "BACKPORT_TARGET=${BASH_REMATCH[1]}" >> "$GITHUB_ENV" + else + echo "Usage: /backport " >&2 + exit 1 + fi + + - name: Create backport pull request + uses: korthout/backport-action@v4 + with: + add_labels: 'backport' + copy_labels_pattern: '.*' + label_pattern: '' + target_branches: ${{ env.BACKPORT_TARGET }} \ No newline at end of file diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 0fad8e169..b558d90dd 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -7,11 +7,11 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: 'master' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -32,7 +32,7 @@ jobs: mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk" - name: "Upload APK" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app path: app/build/outputs/apk/release/*.apk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a184dd83d..d42c5a0b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,8 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: actions/checkout@v6 + - uses: gradle/actions/wrapper-validation@v4 - name: create and checkout branch # push events already checked out the branch @@ -48,7 +48,7 @@ jobs: run: git checkout -B "$BRANCH" - name: set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" @@ -58,7 +58,7 @@ jobs: run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint - name: Upload APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app path: app/build/outputs/apk/debug/*.apk @@ -80,7 +80,7 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Enable KVM run: | @@ -89,7 +89,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" @@ -104,7 +104,7 @@ jobs: 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 + uses: actions/upload-artifact@v6 if: failure() with: name: android-test-report-api${{ matrix.api-level }} @@ -118,19 +118,19 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" cache: 'gradle' - name: Cache SonarCloud packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar diff --git a/.github/workflows/image-minimizer.yml b/.github/workflows/image-minimizer.yml index d9241c33b..264a0ac6c 100644 --- a/.github/workflows/image-minimizer.yml +++ b/.github/workflows/image-minimizer.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 16 @@ -27,7 +27,7 @@ jobs: run: npm i probe-image-size@7.2.3 --ignore-scripts - name: Minimize simple images - uses: actions/github-script@v7 + uses: actions/github-script@v8 timeout-minutes: 3 with: script: | diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt index 8988708e6..6566f7e6a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -34,335 +34,319 @@ object Migrations { private val TAG = Migrations::class.java.getName() private val isDebug = MainActivity.DEBUG - val MIGRATION_1_2 = object : Migration(DB_VER_1, DB_VER_2) { - override fun migrate(db: SupportSQLiteDatabase) { - if (isDebug) { - Log.d(TAG, "Start migrating database") - } + val MIGRATION_1_2 = Migration(DB_VER_1, DB_VER_2) { db -> + if (isDebug) { + Log.d(TAG, "Start migrating database") + } - /* - * Unfortunately these queries must be hardcoded due to the possibility of - * schema and names changing at a later date, thus invalidating the older migration - * scripts if they are not hardcoded. - * */ + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if they are not hardcoded. + * */ - // Not much we can do about this, since room doesn't create tables before migration. - // It's either this or blasting the entire database anew. + // Not much we can do about this, since room doesn't create tables before migration. + // It's either this or blasting the entire database anew. + db.execSQL( + "CREATE INDEX `index_search_history_search` " + + "ON `search_history` (`search`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `streams` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " + + "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " + + "`thumbnail_url` TEXT)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_streams_service_id_url` " + + "ON `streams` (`service_id`, `url`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_history` " + + "(`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 )" + ) + db.execSQL( + "CREATE INDEX `index_stream_history_stream_id` " + + "ON `stream_history` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_state` " + + "(`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 )" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT)" + ) + db.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + + "(`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)" + ) + db.execSQL( + "CREATE UNIQUE INDEX " + + "`index_playlist_stream_join_playlist_id_join_index` " + + "ON `playlist_stream_join` (`playlist_id`, `join_index`)" + ) + db.execSQL( + "CREATE INDEX `index_playlist_stream_join_stream_id` " + + "ON `playlist_stream_join` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)" + ) + db.execSQL( + "CREATE INDEX `index_remote_playlists_name` " + + "ON `remote_playlists` (`name`)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)" + ) + + // Populate streams table with existing entries in watch history + // Latest data first, thus ignoring older entries with the same indices + db.execSQL( + "INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " + + + "FROM watch_history " + + "ORDER BY creation_date DESC" + ) + + // Once the streams have PKs, join them with the normalized history table + // and populate it with the remaining data from watch history + db.execSQL( + "INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC" + ) + + db.execSQL("DROP TABLE IF EXISTS watch_history") + + if (isDebug) { + Log.d(TAG, "Stop migrating database") + } + } + + val MIGRATION_2_3 = Migration(DB_VER_2, DB_VER_3) { db -> + // Add NOT NULLs and new fields + db.execSQL( + "CREATE TABLE IF NOT EXISTS streams_new " + + "(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, thumbnail_url TEXT, view_count INTEGER, " + + "textual_upload_date TEXT, upload_date INTEGER, " + + "is_upload_date_approximation INTEGER)" + ) + + db.execSQL( + "INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + + "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + + "upload_date, is_upload_date_approximation) " + + + "SELECT uid, service_id, url, ifnull(title, ''), " + + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + + + "FROM streams WHERE url IS NOT NULL" + ) + + db.execSQL("DROP TABLE streams") + db.execSQL("ALTER TABLE streams_new RENAME TO streams") + db.execSQL( + "CREATE UNIQUE INDEX index_streams_service_id_url " + + "ON streams (service_id, url)" + ) + + // Tables for feed feature + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed " + + "(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)" + ) + db.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)" + ) + db.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group_subscription_join " + + "(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)" + ) + db.execSQL( + "CREATE INDEX index_feed_group_subscription_join_subscription_id " + + "ON feed_group_subscription_join (subscription_id)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_last_updated " + + "(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)" + ) + } + + val MIGRATION_3_4 = Migration(DB_VER_3, DB_VER_4) { db -> + db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT") + } + + val MIGRATION_4_5 = Migration(DB_VER_4, DB_VER_5) { db -> + db.execSQL( + "ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + + val MIGRATION_5_6 = Migration(DB_VER_5, DB_VER_6) { db -> + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + + val MIGRATION_6_7 = Migration(DB_VER_6, DB_VER_7) { db -> + // Create a new column thumbnail_stream_id + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " + + "INTEGER NOT NULL DEFAULT -1" + ) + + // Migrate the thumbnail_url to the thumbnail_stream_id + db.execSQL( + "UPDATE playlists SET thumbnail_stream_id = (" + + " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" + + " FROM (" + + " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" + + " FROM playlists p" + + " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" + + " LEFT JOIN streams s ON s.uid = ps.stream_id" + + " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" + + " WHERE playlist_uid = playlists.uid)" + ) + + // Remove the thumbnail_url field in the playlist table + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists_new`" + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "name TEXT, " + + "is_thumbnail_permanent INTEGER NOT NULL, " + + "thumbnail_stream_id INTEGER NOT NULL)" + ) + + db.execSQL( + "INSERT INTO playlists_new" + + " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " + + " FROM playlists" + ) + + db.execSQL("DROP TABLE playlists") + db.execSQL("ALTER TABLE playlists_new RENAME TO playlists") + db.execSQL( + "CREATE INDEX IF NOT EXISTS " + + "`index_playlists_name` ON `playlists` (`name`)" + ) + } + + val MIGRATION_7_8 = Migration(DB_VER_7, DB_VER_8) { db -> + db.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)" + ) + db.execSQL("UPDATE search_history SET search = trim(search)") + } + + val MIGRATION_8_9 = Migration(DB_VER_8, DB_VER_9) { db -> + try { + db.beginTransaction() + + // Update playlists. + // Create a temp table to initialize display_index. db.execSQL( - "CREATE INDEX `index_search_history_search` " + - "ON `search_history` (`search`)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `streams` " + + "CREATE TABLE `playlists_tmp` " + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " + - "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " + - "`thumbnail_url` TEXT)" + "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " + + "`thumbnail_stream_id` INTEGER NOT NULL, " + + "`display_index` INTEGER NOT NULL)" ) db.execSQL( - "CREATE UNIQUE INDEX `index_streams_service_id_url` " + - "ON `streams` (`service_id`, `url`)" + "INSERT INTO `playlists_tmp` " + + "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "`display_index`) " + + "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "-1 " + + "FROM `playlists`" ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `playlists`") + db.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`") + + // Update remote_playlists. + // Create a temp table to initialize display_index. db.execSQL( - "CREATE TABLE IF NOT EXISTS `stream_history` " + - "(`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 )" - ) - db.execSQL( - "CREATE INDEX `index_stream_history_stream_id` " + - "ON `stream_history` (`stream_id`)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `stream_state` " + - "(`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 )" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `playlists` " + - "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`name` TEXT, `thumbnail_url` TEXT)" - ) - db.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)") - db.execSQL( - "CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + - "(`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)" - ) - db.execSQL( - "CREATE UNIQUE INDEX " + - "`index_playlist_stream_join_playlist_id_join_index` " + - "ON `playlist_stream_join` (`playlist_id`, `join_index`)" - ) - db.execSQL( - "CREATE INDEX `index_playlist_stream_join_stream_id` " + - "ON `playlist_stream_join` (`stream_id`)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "CREATE TABLE `remote_playlists_tmp` " + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + - "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)" + "`thumbnail_url` TEXT, `uploader` TEXT, " + + "`display_index` INTEGER NOT NULL," + + "`stream_count` INTEGER)" ) db.execSQL( - "CREATE INDEX `index_remote_playlists_name` " + - "ON `remote_playlists` (`name`)" + "INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + + "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " + + "`stream_count`)" + + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + + "-1, `stream_count` FROM `remote_playlists`" ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `remote_playlists`") + db.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`") + + // Create index on the new table. db.execSQL( "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + "ON `remote_playlists` (`service_id`, `url`)" ) - // Populate streams table with existing entries in watch history - // Latest data first, thus ignoring older entries with the same indices - db.execSQL( - "INSERT OR IGNORE INTO streams (service_id, url, title, " + - "stream_type, duration, uploader, thumbnail_url) " + - - "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + - "uploader, thumbnail_url " + - - "FROM watch_history " + - "ORDER BY creation_date DESC" - ) - - // Once the streams have PKs, join them with the normalized history table - // and populate it with the remaining data from watch history - db.execSQL( - "INSERT INTO stream_history (stream_id, access_date, repeat_count)" + - "SELECT uid, creation_date, 1 " + - "FROM watch_history INNER JOIN streams " + - "ON watch_history.service_id == streams.service_id " + - "AND watch_history.url == streams.url " + - "ORDER BY creation_date DESC" - ) - - db.execSQL("DROP TABLE IF EXISTS watch_history") - - if (isDebug) { - Log.d(TAG, "Stop migrating database") - } - } - } - - val MIGRATION_2_3 = object : Migration(DB_VER_2, DB_VER_3) { - override fun migrate(db: SupportSQLiteDatabase) { - // Add NOT NULLs and new fields - db.execSQL( - "CREATE TABLE IF NOT EXISTS streams_new " + - "(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, thumbnail_url TEXT, view_count INTEGER, " + - "textual_upload_date TEXT, upload_date INTEGER, " + - "is_upload_date_approximation INTEGER)" - ) - - db.execSQL( - "INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + - "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + - "upload_date, is_upload_date_approximation) " + - - "SELECT uid, service_id, url, ifnull(title, ''), " + - "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + - "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + - - "FROM streams WHERE url IS NOT NULL" - ) - - db.execSQL("DROP TABLE streams") - db.execSQL("ALTER TABLE streams_new RENAME TO streams") - db.execSQL( - "CREATE UNIQUE INDEX index_streams_service_id_url " + - "ON streams (service_id, url)" - ) - - // Tables for feed feature - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed " + - "(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)" - ) - db.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)") - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed_group " + - "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + - "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)" - ) - db.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)") - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed_group_subscription_join " + - "(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)" - ) - db.execSQL( - "CREATE INDEX index_feed_group_subscription_join_subscription_id " + - "ON feed_group_subscription_join (subscription_id)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed_last_updated " + - "(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)" - ) - } - } - - val MIGRATION_3_4 = object : Migration(DB_VER_3, DB_VER_4) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT") - } - } - - val MIGRATION_4_5 = object : Migration(DB_VER_4, DB_VER_5) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " + - "INTEGER NOT NULL DEFAULT 0" - ) - } - } - - val MIGRATION_5_6 = object : Migration(DB_VER_5, DB_VER_6) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " + - "INTEGER NOT NULL DEFAULT 0" - ) - } - } - - val MIGRATION_6_7 = object : Migration(DB_VER_6, DB_VER_7) { - override fun migrate(db: SupportSQLiteDatabase) { - // Create a new column thumbnail_stream_id - db.execSQL( - "ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " + - "INTEGER NOT NULL DEFAULT -1" - ) - - // Migrate the thumbnail_url to the thumbnail_stream_id - db.execSQL( - "UPDATE playlists SET thumbnail_stream_id = (" + - " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" + - " FROM (" + - " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" + - " FROM playlists p" + - " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" + - " LEFT JOIN streams s ON s.uid = ps.stream_id" + - " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" + - " WHERE playlist_uid = playlists.uid)" - ) - - // Remove the thumbnail_url field in the playlist table - db.execSQL( - "CREATE TABLE IF NOT EXISTS `playlists_new`" + - "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "name TEXT, " + - "is_thumbnail_permanent INTEGER NOT NULL, " + - "thumbnail_stream_id INTEGER NOT NULL)" - ) - - db.execSQL( - "INSERT INTO playlists_new" + - " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " + - " FROM playlists" - ) - - db.execSQL("DROP TABLE playlists") - db.execSQL("ALTER TABLE playlists_new RENAME TO playlists") - db.execSQL( - "CREATE INDEX IF NOT EXISTS " + - "`index_playlists_name` ON `playlists` (`name`)" - ) - } - } - - val MIGRATION_7_8 = object : Migration(DB_VER_7, DB_VER_8) { - override fun migrate(db: SupportSQLiteDatabase) { - db.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)" - ) - db.execSQL("UPDATE search_history SET search = trim(search)") - } - } - - val MIGRATION_8_9 = object : Migration(DB_VER_8, DB_VER_9) { - override fun migrate(db: SupportSQLiteDatabase) { - try { - db.beginTransaction() - - // Update playlists. - // Create a temp table to initialize display_index. - db.execSQL( - "CREATE TABLE `playlists_tmp` " + - "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " + - "`thumbnail_stream_id` INTEGER NOT NULL, " + - "`display_index` INTEGER NOT NULL)" - ) - db.execSQL( - "INSERT INTO `playlists_tmp` " + - "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + - "`display_index`) " + - "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + - "-1 " + - "FROM `playlists`" - ) - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - db.execSQL("DROP TABLE `playlists`") - db.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`") - - // Update remote_playlists. - // Create a temp table to initialize display_index. - db.execSQL( - "CREATE TABLE `remote_playlists_tmp` " + - "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + - "`thumbnail_url` TEXT, `uploader` TEXT, " + - "`display_index` INTEGER NOT NULL," + - "`stream_count` INTEGER)" - ) - db.execSQL( - "INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + - "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " + - "`stream_count`)" + - "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + - "-1, `stream_count` FROM `remote_playlists`" - ) - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - db.execSQL("DROP TABLE `remote_playlists`") - db.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`") - - // Create index on the new table. - db.execSQL( - "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + - "ON `remote_playlists` (`service_id`, `url`)" - ) - - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } + db.setTransactionSuccessful() + } finally { + db.endTransaction() } } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt index 8e0b80c3b..36a80bc91 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt @@ -28,7 +28,7 @@ interface PlaylistRemoteDAO : BasicDAO { @Query("SELECT * FROM remote_playlists WHERE uid = :playlistId") fun getPlaylist(playlistId: Long): Flowable - @Query("SELECT * FROM remote_playlists WHERE url = :url AND uid = :serviceId") + @Query("SELECT * FROM remote_playlists WHERE url = :url AND service_id = :serviceId") fun getPlaylist(serviceId: Long, url: String?): Flowable> @get:Query("SELECT * FROM remote_playlists ORDER BY display_index") diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.kt similarity index 86% rename from app/src/main/java/org/schabi/newpipe/error/UserAction.java rename to app/src/main/java/org/schabi/newpipe/error/UserAction.kt index d3af9d32e..2d2358310 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.kt @@ -1,9 +1,14 @@ -package org.schabi.newpipe.error; +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.error /** * The user actions that can cause an error. */ -public enum UserAction { +enum class UserAction(val message: String) { USER_REPORT("user report"), UI_ERROR("ui error"), DATABASE_IMPORT_EXPORT("database import or export"), @@ -36,14 +41,4 @@ public enum UserAction { GETTING_MAIN_SCREEN_TAB("getting main screen tab"), PLAY_ON_POPUP("play on popup"), SUBSCRIPTIONS("loading subscriptions"); - - private final String message; - - UserAction(final String message) { - this.message = message; - } - - public String getMessage() { - return message; - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java deleted file mode 100644 index 83f68dbb5..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.schabi.newpipe.fragments.list.search; - -import androidx.annotation.NonNull; - -public class SuggestionItem { - final boolean fromHistory; - public final String query; - - public SuggestionItem(final boolean fromHistory, final String query) { - this.fromHistory = fromHistory; - this.query = query; - } - - @Override - public boolean equals(final Object o) { - if (o instanceof SuggestionItem) { - return query.equals(((SuggestionItem) o).query); - } - return false; - } - - @Override - public int hashCode() { - return query.hashCode(); - } - - @NonNull - @Override - public String toString() { - return "[" + fromHistory + "→" + query + "]"; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt new file mode 100644 index 000000000..1317f9acb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.fragments.list.search + +class SuggestionItem(@JvmField val fromHistory: Boolean, @JvmField val query: String) { + override fun equals(other: Any?): Boolean { + if (other is SuggestionItem) { + return query == other.query + } + return false + } + + override fun hashCode() = query.hashCode() + + override fun toString() = "[$fromHistory→$query]" +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java deleted file mode 100644 index 6a330be0f..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.schabi.newpipe.fragments.list.search; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding; - -public class SuggestionListAdapter - extends ListAdapter { - private OnSuggestionItemSelected listener; - - public SuggestionListAdapter() { - super(new SuggestionItemCallback()); - } - - public void setListener(final OnSuggestionItemSelected listener) { - this.listener = listener; - } - - @NonNull - @Override - public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent, - final int viewType) { - return new SuggestionItemHolder(ItemSearchSuggestionBinding - .inflate(LayoutInflater.from(parent.getContext()), parent, false)); - } - - @Override - public void onBindViewHolder(final SuggestionItemHolder holder, final int position) { - final SuggestionItem currentItem = getItem(position); - holder.updateFrom(currentItem); - holder.itemBinding.suggestionSearch.setOnClickListener(v -> { - if (listener != null) { - listener.onSuggestionItemSelected(currentItem); - } - }); - holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> { - if (listener != null) { - listener.onSuggestionItemLongClick(currentItem); - } - return true; - }); - holder.itemBinding.suggestionInsert.setOnClickListener(v -> { - if (listener != null) { - listener.onSuggestionItemInserted(currentItem); - } - }); - } - - public interface OnSuggestionItemSelected { - void onSuggestionItemSelected(SuggestionItem item); - - void onSuggestionItemInserted(SuggestionItem item); - - void onSuggestionItemLongClick(SuggestionItem item); - } - - public static final class SuggestionItemHolder extends RecyclerView.ViewHolder { - private final ItemSearchSuggestionBinding itemBinding; - - private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) { - super(binding.getRoot()); - this.itemBinding = binding; - } - - private void updateFrom(final SuggestionItem item) { - itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history - : R.drawable.ic_search); - itemBinding.itemSuggestionQuery.setText(item.query); - } - } - - private static final class SuggestionItemCallback - extends DiffUtil.ItemCallback { - @Override - public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem, - @NonNull final SuggestionItem newItem) { - return oldItem.fromHistory == newItem.fromHistory - && oldItem.query.equals(newItem.query); - } - - @Override - public boolean areContentsTheSame(@NonNull final SuggestionItem oldItem, - @NonNull final SuggestionItem newItem) { - return true; // items' contents never change; the list of items themselves does - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt new file mode 100644 index 000000000..4eb4c1574 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.fragments.list.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding +import org.schabi.newpipe.fragments.list.search.SuggestionListAdapter.SuggestionItemHolder + +class SuggestionListAdapter : + ListAdapter(SuggestionItemCallback()) { + + var listener: OnSuggestionItemSelected? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionItemHolder { + return SuggestionItemHolder( + ItemSearchSuggestionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + override fun onBindViewHolder(holder: SuggestionItemHolder, position: Int) { + val currentItem = getItem(position) + holder.updateFrom(currentItem) + holder.binding.suggestionSearch.setOnClickListener { + listener?.onSuggestionItemSelected(currentItem) + } + holder.binding.suggestionSearch.setOnLongClickListener { + listener?.onSuggestionItemLongClick(currentItem) + true + } + holder.binding.suggestionInsert.setOnClickListener { + listener?.onSuggestionItemInserted(currentItem) + } + } + + interface OnSuggestionItemSelected { + fun onSuggestionItemSelected(item: SuggestionItem) + + fun onSuggestionItemInserted(item: SuggestionItem) + + fun onSuggestionItemLongClick(item: SuggestionItem) + } + + class SuggestionItemHolder(val binding: ItemSearchSuggestionBinding) : + RecyclerView.ViewHolder(binding.getRoot()) { + fun updateFrom(item: SuggestionItem) { + binding.itemSuggestionIcon.setImageResource( + if (item.fromHistory) { + R.drawable.ic_history + } else { + R.drawable.ic_search + } + ) + binding.itemSuggestionQuery.text = item.query + } + } + + private class SuggestionItemCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SuggestionItem, newItem: SuggestionItem): Boolean { + return oldItem.fromHistory == newItem.fromHistory && oldItem.query == newItem.query + } + + override fun areContentsTheSame(oldItem: SuggestionItem, newItem: SuggestionItem): Boolean { + return true // items' contents never change; the list of items themselves does + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.java b/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.kt similarity index 65% rename from app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.java rename to app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.kt index 447c540a0..703191bb9 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.kt @@ -1,9 +1,14 @@ -package org.schabi.newpipe.info_list; +/* + * SPDX-FileCopyrightText: 2023-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.info_list /** * Item view mode for streams & playlist listing screens. */ -public enum ItemViewMode { +enum class ItemViewMode { /** * Default mode. */ diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java deleted file mode 100644 index 709a16b68..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.schabi.newpipe.local.history; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import org.schabi.newpipe.util.Localization; - -import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; - - -/** - * This is an adapter for history entries. - * - * @param the type of the entries - * @param the type of the view holder - */ -public abstract class HistoryEntryAdapter - extends RecyclerView.Adapter { - private final ArrayList mEntries; - private final DateFormat mDateFormat; - private final Context mContext; - private OnHistoryItemClickListener onHistoryItemClickListener = null; - - public HistoryEntryAdapter(final Context context) { - super(); - mContext = context; - mEntries = new ArrayList<>(); - mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, - Localization.getPreferredLocale(context)); - } - - public void setEntries(@NonNull final Collection historyEntries) { - mEntries.clear(); - mEntries.addAll(historyEntries); - notifyDataSetChanged(); - } - - public Collection getItems() { - return mEntries; - } - - public void clear() { - mEntries.clear(); - notifyDataSetChanged(); - } - - protected String getFormattedDate(final Date date) { - return mDateFormat.format(date); - } - - protected String getFormattedViewString(final long viewCount) { - return Localization.shortViewCount(mContext, viewCount); - } - - @Override - public int getItemCount() { - return mEntries.size(); - } - - @Override - public void onBindViewHolder(final VH holder, final int position) { - final E entry = mEntries.get(position); - holder.itemView.setOnClickListener(v -> { - if (onHistoryItemClickListener != null) { - onHistoryItemClickListener.onHistoryItemClick(entry); - } - }); - - holder.itemView.setOnLongClickListener(view -> { - if (onHistoryItemClickListener != null) { - onHistoryItemClickListener.onHistoryItemLongClick(entry); - return true; - } - return false; - }); - - onBindViewHolder(holder, entry, position); - } - - @Override - public void onViewRecycled(@NonNull final VH holder) { - super.onViewRecycled(holder); - holder.itemView.setOnClickListener(null); - } - - abstract void onBindViewHolder(VH holder, E entry, int position); - - public void setOnHistoryItemClickListener( - @Nullable final OnHistoryItemClickListener onHistoryItemClickListener) { - this.onHistoryItemClickListener = onHistoryItemClickListener; - } - - public boolean isEmpty() { - return mEntries.isEmpty(); - } - - public interface OnHistoryItemClickListener { - void onHistoryItemClick(E item); - - void onHistoryItemLongClick(E item); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt b/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt index 0d4dcbfd0..8eb3ab3ae 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.local.playlist import android.content.Context @@ -21,11 +26,7 @@ fun export( } } -fun exportWithTitles( - playlist: List, - context: Context -): String { - +private fun exportWithTitles(playlist: List, context: Context): String { return playlist.asSequence() .map { it.streamEntity } .map { entity -> @@ -38,18 +39,14 @@ fun exportWithTitles( .joinToString(separator = "\n") } -fun exportJustUrls(playlist: List): String { - - return playlist.asSequence() - .map { it.streamEntity.url } - .joinToString(separator = "\n") +private fun exportJustUrls(playlist: List): String { + return playlist.joinToString(separator = "\n") { it.streamEntity.url } } -fun exportAsYoutubeTempPlaylist(playlist: List): String { +private fun exportAsYoutubeTempPlaylist(playlist: List): String { val videoIDs = playlist.asReversed().asSequence() - .map { it.streamEntity.url } - .mapNotNull(::getYouTubeId) + .mapNotNull { getYouTubeId(it.streamEntity.url) } .take(50) // YouTube limitation: temp playlists can't have more than 50 items .toList() .asReversed() @@ -58,7 +55,7 @@ fun exportAsYoutubeTempPlaylist(playlist: List): String { return "https://www.youtube.com/watch_videos?video_ids=$videoIDs" } -val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance() +private val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance() /** * Gets the video id from a YouTube URL. @@ -66,7 +63,7 @@ val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFacto * @param url YouTube URL * @return the video id */ -fun getYouTubeId(url: String): String? { +private fun getYouTubeId(url: String): String? { return try { linkHandler.getId(url) } catch (e: ParsingException) { null } } 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 f5562549c..1efc0a84c 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 @@ -111,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { @Override public void selected(final LocalItem selectedItem) { - if (selectedItem instanceof PlaylistStreamEntry) { - final StreamEntity item = - ((PlaylistStreamEntry) selectedItem).getStreamEntity(); + if (selectedItem instanceof PlaylistStreamEntry entry) { + final StreamEntity item = entry.getStreamEntity(); NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), item.getServiceId(), item.getUrl(), item.getTitle(), null, false); } @@ -496,6 +495,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment items = itemListAdapter.getItemsList(); final List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { - if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).getStreamId()); + if (item instanceof PlaylistStreamEntry entry) { + streamIds.add(entry.getStreamId()); } } @@ -767,6 +768,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.local.playlist + +enum class PlayListShareMode { + JUST_URLS, + WITH_TITLES, + YOUTUBE_TEMP_PLAYLIST +} diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java deleted file mode 100644 index 08b203a7e..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.schabi.newpipe.local.playlist; - -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; - -import java.util.List; - -import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public class RemotePlaylistManager { - - private final AppDatabase database; - private final PlaylistRemoteDAO playlistRemoteTable; - - public RemotePlaylistManager(final AppDatabase db) { - database = db; - playlistRemoteTable = db.playlistRemoteDAO(); - } - - public Flowable> getPlaylists() { - return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io()); - } - - public Flowable getPlaylist(final long playlistId) { - return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io()); - } - - public Flowable> getPlaylist(final PlaylistInfo info) { - return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) - .subscribeOn(Schedulers.io()); - } - - public Single deletePlaylist(final long playlistId) { - return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId)) - .subscribeOn(Schedulers.io()); - } - - public Completable updatePlaylists(final List updateItems, - final List deletedItems) { - return Completable.fromRunnable(() -> database.runInTransaction(() -> { - for (final Long uid: deletedItems) { - playlistRemoteTable.deletePlaylist(uid); - } - for (final PlaylistRemoteEntity item: updateItems) { - playlistRemoteTable.upsert(item); - } - })).subscribeOn(Schedulers.io()); - } - - public Single onBookmark(final PlaylistInfo playlistInfo) { - return Single.fromCallable(() -> { - final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); - return playlistRemoteTable.upsert(playlist); - }).subscribeOn(Schedulers.io()); - } - - public Single onUpdate(final long playlistId, final PlaylistInfo playlistInfo) { - return Single.fromCallable(() -> { - final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); - playlist.setUid(playlistId); - return playlistRemoteTable.update(playlist); - }).subscribeOn(Schedulers.io()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt new file mode 100644 index 000000000..6961b6bb4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.local.playlist + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.database.AppDatabase +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity +import org.schabi.newpipe.extractor.playlist.PlaylistInfo + +class RemotePlaylistManager(private val database: AppDatabase) { + private val playlistRemoteTable = database.playlistRemoteDAO() + + val playlists: Flowable> + get() = playlistRemoteTable.playlists.subscribeOn(Schedulers.io()) + + fun getPlaylist(playlistId: Long): Flowable { + return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io()) + } + + fun getPlaylist(info: PlaylistInfo): Flowable> { + return playlistRemoteTable.getPlaylist(info.serviceId.toLong(), info.url) + .subscribeOn(Schedulers.io()) + } + + fun deletePlaylist(playlistId: Long): Single { + return Single.fromCallable { playlistRemoteTable.deletePlaylist(playlistId) } + .subscribeOn(Schedulers.io()) + } + + fun updatePlaylists( + updateItems: List, + deletedItems: List + ): Completable { + return Completable.fromRunnable { + database.runInTransaction { + deletedItems.forEach { playlistRemoteTable.deletePlaylist(it) } + updateItems.forEach { playlistRemoteTable.upsert(it) } + } + }.subscribeOn(Schedulers.io()) + } + + fun onBookmark(playlistInfo: PlaylistInfo): Single { + return Single.fromCallable { + val playlist = PlaylistRemoteEntity(playlistInfo) + playlistRemoteTable.upsert(playlist) + }.subscribeOn(Schedulers.io()) + } + + fun onUpdate(playlistId: Long, playlistInfo: PlaylistInfo): Single { + return Single.fromCallable { + val playlist = PlaylistRemoteEntity(playlistInfo).apply { uid = playlistId } + playlistRemoteTable.update(playlist) + }.subscribeOn(Schedulers.io()) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java deleted file mode 100644 index f74389d79..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.player; - -public enum PlayerType { - MAIN, - AUDIO, - POPUP; -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerType.kt new file mode 100644 index 000000000..42b2e1131 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerType.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.player + +enum class PlayerType { + MAIN, + AUDIO, + POPUP +} 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 9092906fa..f1371cd1e 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 @@ -17,10 +17,10 @@ import org.schabi.newpipe.player.mediasource.ManagedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.events.MoveEvent; -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; -import org.schabi.newpipe.player.playqueue.events.RemoveEvent; -import org.schabi.newpipe.player.playqueue.events.ReorderEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent; import java.util.Collection; import java.util.Collections; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt index ab7dee2d0..e4f4ef1b2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt @@ -4,15 +4,14 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.subjects.BehaviorSubject -import org.schabi.newpipe.player.playqueue.events.AppendEvent -import org.schabi.newpipe.player.playqueue.events.ErrorEvent -import org.schabi.newpipe.player.playqueue.events.InitEvent -import org.schabi.newpipe.player.playqueue.events.MoveEvent -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent -import org.schabi.newpipe.player.playqueue.events.RecoveryEvent -import org.schabi.newpipe.player.playqueue.events.RemoveEvent -import org.schabi.newpipe.player.playqueue.events.ReorderEvent -import org.schabi.newpipe.player.playqueue.events.SelectEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RecoveryEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent import java.io.Serializable import java.util.Collections import java.util.concurrent.atomic.AtomicInteger diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java index dd95fb4d5..2e19672e5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java @@ -10,12 +10,11 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.R; -import org.schabi.newpipe.player.playqueue.events.AppendEvent; -import org.schabi.newpipe.player.playqueue.events.ErrorEvent; -import org.schabi.newpipe.player.playqueue.events.MoveEvent; -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; -import org.schabi.newpipe.player.playqueue.events.RemoveEvent; -import org.schabi.newpipe.player.playqueue.events.SelectEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent; import org.schabi.newpipe.util.FallbackViewHolder; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt new file mode 100644 index 000000000..f1952ef95 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2017-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.player.playqueue + +import java.io.Serializable + +sealed interface PlayQueueEvent : Serializable { + fun type(): Type + + class InitEvent : PlayQueueEvent { + override fun type() = Type.INIT + } + + // sent when the index is changed + class SelectEvent(val oldIndex: Int, val newIndex: Int) : PlayQueueEvent { + override fun type() = Type.SELECT + } + + // sent when more streams are added to the play queue + class AppendEvent(val amount: Int) : PlayQueueEvent { + override fun type() = Type.APPEND + } + + // sent when a pending stream is removed from the play queue + class RemoveEvent(val removeIndex: Int, val queueIndex: Int) : PlayQueueEvent { + override fun type() = Type.REMOVE + } + + // sent when two streams swap place in the play queue + class MoveEvent(val fromIndex: Int, val toIndex: Int) : PlayQueueEvent { + override fun type() = Type.MOVE + } + + // sent when queue is shuffled + class ReorderEvent(val fromSelectedIndex: Int, val toSelectedIndex: Int) : PlayQueueEvent { + override fun type() = Type.REORDER + } + + // sent when recovery record is set on a stream + class RecoveryEvent(val index: Int, val position: Long) : PlayQueueEvent { + override fun type() = Type.RECOVERY + } + + // sent when the item at index has caused an exception + class ErrorEvent(val errorIndex: Int, val queueIndex: Int) : PlayQueueEvent { + override fun type() = Type.ERROR + } + + // It is necessary only for use in java code. Remove it and use kotlin pattern + // matching when all users of this enum are converted to kotlin + enum class Type { INIT, SELECT, APPEND, REMOVE, MOVE, REORDER, RECOVERY, ERROR } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java deleted file mode 100644 index cc922dbb1..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class AppendEvent implements PlayQueueEvent { - private final int amount; - - public AppendEvent(final int amount) { - this.amount = amount; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.APPEND; - } - - public int getAmount() { - return amount; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java deleted file mode 100644 index 7b7e39212..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class ErrorEvent implements PlayQueueEvent { - private final int errorIndex; - private final int queueIndex; - - public ErrorEvent(final int errorIndex, final int queueIndex) { - this.errorIndex = errorIndex; - this.queueIndex = queueIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.ERROR; - } - - public int getErrorIndex() { - return errorIndex; - } - - public int getQueueIndex() { - return queueIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java deleted file mode 100644 index 559975b35..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class InitEvent implements PlayQueueEvent { - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.INIT; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java deleted file mode 100644 index 55d198923..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class MoveEvent implements PlayQueueEvent { - private final int fromIndex; - private final int toIndex; - - public MoveEvent(final int oldIndex, final int newIndex) { - this.fromIndex = oldIndex; - this.toIndex = newIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.MOVE; - } - - public int getFromIndex() { - return fromIndex; - } - - public int getToIndex() { - return toIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java deleted file mode 100644 index 431053e7b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -import java.io.Serializable; - -public interface PlayQueueEvent extends Serializable { - PlayQueueEventType type(); -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java deleted file mode 100644 index 1cc710c7b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public enum PlayQueueEventType { - INIT, - - // sent when the index is changed - SELECT, - - // sent when more streams are added to the play queue - APPEND, - - // sent when a pending stream is removed from the play queue - REMOVE, - - // sent when two streams swap place in the play queue - MOVE, - - // sent when queue is shuffled - REORDER, - - // sent when recovery record is set on a stream - RECOVERY, - - // sent when the item at index has caused an exception - ERROR -} - diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java deleted file mode 100644 index 6f21b36cd..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class RecoveryEvent implements PlayQueueEvent { - private final int index; - private final long position; - - public RecoveryEvent(final int index, final long position) { - this.index = index; - this.position = position; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.RECOVERY; - } - - public int getIndex() { - return index; - } - - public long getPosition() { - return position; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java deleted file mode 100644 index a5872906d..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class RemoveEvent implements PlayQueueEvent { - private final int removeIndex; - private final int queueIndex; - - public RemoveEvent(final int removeIndex, final int queueIndex) { - this.removeIndex = removeIndex; - this.queueIndex = queueIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.REMOVE; - } - - public int getQueueIndex() { - return queueIndex; - } - - public int getRemoveIndex() { - return removeIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java deleted file mode 100644 index 4f4f14756..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class ReorderEvent implements PlayQueueEvent { - private final int fromSelectedIndex; - private final int toSelectedIndex; - - public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) { - this.fromSelectedIndex = fromSelectedIndex; - this.toSelectedIndex = toSelectedIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.REORDER; - } - - public int getFromSelectedIndex() { - return fromSelectedIndex; - } - - public int getToSelectedIndex() { - return toSelectedIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java deleted file mode 100644 index 95e344211..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class SelectEvent implements PlayQueueEvent { - private final int oldIndex; - private final int newIndex; - - public SelectEvent(final int oldIndex, final int newIndex) { - this.oldIndex = oldIndex; - this.newIndex = newIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.SELECT; - } - - public int getOldIndex() { - return oldIndex; - } - - public int getNewIndex() { - return newIndex; - } -} 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 59be1d67d..61f9dfa3f 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 @@ -1557,6 +1557,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa @Override public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { super.onVideoSizeChanged(videoSize); + // Starting with ExoPlayer 2.19.0, the VideoSize will report a width and height of 0 + // if the renderer is disabled. In that case, we skip updating the aspect ratio. + if (videoSize.width == 0 || videoSize.height == 0) { + return; + } binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height); } //endregion diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index 20e639b6f..dc4a403d2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -92,10 +92,9 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { return true; }); - final Preference resetSettings = findPreference(getString(R.string.reset_settings)); + final Preference resetSettings = requirePreference(R.string.reset_settings); // Resets all settings by deleting shared preference and restarting the app // A dialogue will pop up to confirm if user intends to reset all settings - assert resetSettings != null; resetSettings.setOnPreferenceClickListener(preference -> { // Show Alert Dialogue final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index 619579f3a..21cba3daa 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -48,8 +48,8 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { } @NonNull - public final Preference requirePreference(@StringRes final int resId) { - final Preference preference = findPreference(getString(resId)); + public final T requirePreference(@StringRes final int resId) { + final T preference = findPreference(getString(resId)); Objects.requireNonNull(preference); return preference; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index c6abb5405..229de7005 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -21,24 +21,18 @@ public class DebugSettingsFragment extends BasePreferenceFragment { addPreferencesFromResourceRegistry(); final Preference allowHeapDumpingPreference = - findPreference(getString(R.string.allow_heap_dumping_key)); + requirePreference(R.string.allow_heap_dumping_key); final Preference showMemoryLeaksPreference = - findPreference(getString(R.string.show_memory_leaks_key)); + requirePreference(R.string.show_memory_leaks_key); final Preference checkNewStreamsPreference = - findPreference(getString(R.string.check_new_streams_key)); + requirePreference(R.string.check_new_streams_key); final Preference crashTheAppPreference = - findPreference(getString(R.string.crash_the_app_key)); + requirePreference(R.string.crash_the_app_key); final Preference showErrorSnackbarPreference = - findPreference(getString(R.string.show_error_snackbar_key)); + requirePreference(R.string.show_error_snackbar_key); final Preference createErrorNotificationPreference = - findPreference(getString(R.string.create_error_notification_key)); + requirePreference(R.string.create_error_notification_key); - assert allowHeapDumpingPreference != null; - assert showMemoryLeaksPreference != null; - assert checkNewStreamsPreference != null; - assert crashTheAppPreference != null; - assert showErrorSnackbarPreference != null; - assert createErrorNotificationPreference != null; final Optional optBVLeakCanary = getBVDLeakCanary(); 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 32e33d55b..cb3de39a0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -25,7 +25,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Check if the app is updatable if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) { getPreferenceScreen().removePreference( - findPreference(getString(R.string.update_pref_screen_key))); + requirePreference(R.string.update_pref_screen_key)); defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); } @@ -33,7 +33,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Hide debug preferences in RELEASE build variant if (!DEBUG) { getPreferenceScreen().removePreference( - findPreference(getString(R.string.debug_pref_screen_key))); + requirePreference(R.string.debug_pref_screen_key)); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt index 2d3344c09..d6b0a84da 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt @@ -29,8 +29,7 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.notifications_settings) - streamsNotificationsPreference = - findPreference(getString(R.string.enable_streams_notifications)) + streamsNotificationsPreference = requirePreference(R.string.enable_streams_notifications) // main check is done in onResume, but also do it here to prevent flickering updateEnabledState(NotificationHelper.areNotificationsEnabledOnDevice(requireContext())) @@ -125,8 +124,8 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen private fun updateSubscriptions(subscriptions: List) { val notified = subscriptions.count { it.notificationMode != NotificationMode.DISABLED } - val preference = findPreference(getString(R.string.streams_notifications_channels_key)) - preference?.apply { summary = "$notified/${subscriptions.size}" } + val preference = requirePreference(R.string.streams_notifications_channels_key) + preference.summary = "$notified/${subscriptions.size}" } private fun onError(e: Throwable) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index b8d0aa556..8923972b0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -34,9 +34,9 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResourceRegistry(); - findPreference(getString(R.string.update_app_key)) + requirePreference(R.string.update_app_key) .setOnPreferenceChangeListener(updatePreferenceChange); - findPreference(getString(R.string.manual_update_key)) + requirePreference(R.string.manual_update_key) .setOnPreferenceClickListener(manualUpdateClick); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index a1f563724..c5c4c480c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -90,12 +90,12 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { showHigherResolutions); // get resolution preferences - final ListPreference defaultResolution = findPreference( - getString(R.string.default_resolution_key)); - final ListPreference defaultPopupResolution = findPreference( - getString(R.string.default_popup_resolution_key)); - final ListPreference mobileDataResolution = findPreference( - getString(R.string.limit_mobile_data_usage_key)); + final ListPreference defaultResolution = requirePreference( + R.string.default_resolution_key); + final ListPreference defaultPopupResolution = requirePreference( + R.string.default_popup_resolution_key); + final ListPreference mobileDataResolution = requirePreference( + R.string.limit_mobile_data_usage_key); // update resolution preferences with new resolutions, entries & values for each defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0])); @@ -161,8 +161,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { } } - final ListPreference durations = findPreference( - getString(R.string.seek_duration_key)); + final ListPreference durations = requirePreference(R.string.seek_duration_key); durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0])); durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0])); final int selectedDuration = Integer.parseInt(durations.getValue()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java deleted file mode 100644 index 33856326c..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.schabi.newpipe.settings.preferencesearch; - -import androidx.annotation.NonNull; -import androidx.annotation.XmlRes; - -import java.util.List; -import java.util.Objects; - -/** - * Represents a preference-item inside the search. - */ -public class PreferenceSearchItem { - /** - * Key of the setting/preference. E.g. used inside {@link android.content.SharedPreferences}. - */ - @NonNull - private final String key; - /** - * Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'. - */ - @NonNull - private final String title; - /** - * Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'. - */ - @NonNull - private final String summary; - /** - * Possible entries of the setting, e.g. 480p,720p,... - */ - @NonNull - private final String entries; - /** - * Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player' - */ - @NonNull - private final String breadcrumbs; - /** - * The xml-resource where this item was found/built from. - */ - @XmlRes - private final int searchIndexItemResId; - - public PreferenceSearchItem( - @NonNull final String key, - @NonNull final String title, - @NonNull final String summary, - @NonNull final String entries, - @NonNull final String breadcrumbs, - @XmlRes final int searchIndexItemResId - ) { - this.key = Objects.requireNonNull(key); - this.title = Objects.requireNonNull(title); - this.summary = Objects.requireNonNull(summary); - this.entries = Objects.requireNonNull(entries); - this.breadcrumbs = Objects.requireNonNull(breadcrumbs); - this.searchIndexItemResId = searchIndexItemResId; - } - - @NonNull - public String getKey() { - return key; - } - - @NonNull - public String getTitle() { - return title; - } - - @NonNull - public String getSummary() { - return summary; - } - - @NonNull - public String getEntries() { - return entries; - } - - @NonNull - public String getBreadcrumbs() { - return breadcrumbs; - } - - public int getSearchIndexItemResId() { - return searchIndexItemResId; - } - - boolean hasData() { - return !key.isEmpty() && !title.isEmpty(); - } - - public List getAllRelevantSearchFields() { - return List.of(getTitle(), getSummary(), getEntries(), getBreadcrumbs()); - } - - @NonNull - @Override - public String toString() { - return "PreferenceItem: " + title + " " + summary + " " + key; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt new file mode 100644 index 000000000..750e40eae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2022-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.preferencesearch + +import androidx.annotation.XmlRes + +/** + * Represents a preference-item inside the search. + * + * @param key Key of the setting/preference. E.g. used inside [android.content.SharedPreferences]. + * @param title Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'. + * @param summary Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'. + * @param entries Possible entries of the setting, e.g. 480p,720p,... + * @param breadcrumbs Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player' + * @param searchIndexItemResId The xml-resource where this item was found/built from. + */ + +data class PreferenceSearchItem( + val key: String, + val title: String, + val summary: String, + val entries: String, + val breadcrumbs: String, + @XmlRes val searchIndexItemResId: Int +) { + fun hasData(): Boolean { + return !key.isEmpty() && !title.isEmpty() + } + + fun getAllRelevantSearchFields(): MutableList { + return mutableListOf(title, summary, entries, breadcrumbs) + } + + override fun toString(): String { + return "PreferenceItem: $title $summary $key" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java deleted file mode 100644 index 1f0636454..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.settings.preferencesearch; - -import androidx.annotation.NonNull; - -public interface PreferenceSearchResultListener { - void onSearchResultClicked(@NonNull PreferenceSearchItem result); -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt new file mode 100644 index 000000000..7b7b7884a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2022-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.preferencesearch + +interface PreferenceSearchResultListener { + fun onSearchResultClicked(result: PreferenceSearchItem) +} diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java deleted file mode 100644 index bc15f3f02..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.preference.PreferenceManager; - -import org.schabi.newpipe.R; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class FilenameUtils { - private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"; - private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+"; - - private FilenameUtils() { } - - /** - * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. - * - * @param context the context to retrieve strings and preferences from - * @param title the title to create a filename from - * @return the filename - */ - public static String createFilename(final Context context, final String title) { - final SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(context); - - final String charsetLd = context.getString(R.string.charset_letters_and_digits_value); - final String charsetMs = context.getString(R.string.charset_most_special_value); - final String defaultCharset = context.getString(R.string.default_file_charset_value); - - final String replacementChar = sharedPreferences.getString( - context.getString(R.string.settings_file_replacement_character_key), "_"); - String selectedCharset = sharedPreferences.getString( - context.getString(R.string.settings_file_charset_key), null); - - final String charset; - - if (selectedCharset == null || selectedCharset.isEmpty()) { - selectedCharset = defaultCharset; - } - - if (selectedCharset.equals(charsetLd)) { - charset = CHARSET_ONLY_LETTERS_AND_DIGITS; - } else if (selectedCharset.equals(charsetMs)) { - charset = CHARSET_MOST_SPECIAL; - } else { - charset = selectedCharset; // Is the user using a custom charset? - } - - final Pattern pattern = Pattern.compile(charset); - - return createFilename(title, pattern, Matcher.quoteReplacement(replacementChar)); - } - - /** - * Create a valid filename. - * - * @param title the title to create a filename from - * @param invalidCharacters patter matching invalid characters - * @param replacementChar the replacement - * @return the filename - */ - private static String createFilename(final String title, final Pattern invalidCharacters, - final String replacementChar) { - return title.replaceAll(invalidCharacters.pattern(), replacementChar); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt new file mode 100644 index 000000000..bfa50beef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util + +import android.content.Context +import androidx.preference.PreferenceManager +import org.schabi.newpipe.R +import org.schabi.newpipe.ktx.getStringSafe +import java.util.regex.Matcher + +object FilenameUtils { + private const val CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+" + private const val CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+" + + /** + * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. + * + * @param context the context to retrieve strings and preferences from + * @param title the title to create a filename from + * @return the filename + */ + @JvmStatic + fun createFilename(context: Context, title: String): String { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + val charsetLd = context.getString(R.string.charset_letters_and_digits_value) + val charsetMs = context.getString(R.string.charset_most_special_value) + val defaultCharset = context.getString(R.string.default_file_charset_value) + + val replacementChar = sharedPreferences.getStringSafe( + context.getString(R.string.settings_file_replacement_character_key), "_" + ) + val selectedCharset = sharedPreferences.getStringSafe( + context.getString(R.string.settings_file_charset_key), "" + ).ifEmpty { defaultCharset } + + val charset = when (selectedCharset) { + charsetLd -> CHARSET_ONLY_LETTERS_AND_DIGITS + charsetMs -> CHARSET_MOST_SPECIAL + else -> selectedCharset // Is the user using a custom charset? + } + + return createFilename(title, charset, Matcher.quoteReplacement(replacementChar)) + } + + /** + * Create a valid filename. + * + * @param title the title to create a filename from + * @param invalidCharacters patter matching invalid characters + * @param replacementChar the replacement + * @return the filename + */ + private fun createFilename( + title: String, + invalidCharacters: String, + replacementChar: String + ): String { + return title.replace(invalidCharacters.toRegex(), replacementChar) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt index 39167e969..1f86f5db7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt @@ -1,50 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.util import android.content.Context import org.schabi.newpipe.R -/** - * Created by Christian Schabesberger on 28.09.17. - * KioskTranslator.java is part of NewPipe. - * - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see //www.gnu.org/licenses/>. - * - */ object KioskTranslator { @JvmStatic - fun getTranslatedKioskName(kioskId: String, c: Context): String { + fun getTranslatedKioskName(kioskId: String, context: Context): String { return when (kioskId) { - "Trending" -> c.getString(R.string.trending) - "Top 50" -> c.getString(R.string.top_50) - "New & hot" -> c.getString(R.string.new_and_hot) - "Local" -> c.getString(R.string.local) - "Recently added" -> c.getString(R.string.recently_added) - "Most liked" -> c.getString(R.string.most_liked) - "conferences" -> c.getString(R.string.conferences) - "recent" -> c.getString(R.string.recent) - "live" -> c.getString(R.string.duration_live) - "Featured" -> c.getString(R.string.featured) - "Radio" -> c.getString(R.string.radio) - "trending_gaming" -> c.getString(R.string.trending_gaming) - "trending_music" -> c.getString(R.string.trending_music) - "trending_movies_and_shows" -> c.getString(R.string.trending_movies) - "trending_podcasts_episodes" -> c.getString(R.string.trending_podcasts) + "Trending" -> context.getString(R.string.trending) + "Top 50" -> context.getString(R.string.top_50) + "New & hot" -> context.getString(R.string.new_and_hot) + "Local" -> context.getString(R.string.local) + "Recently added" -> context.getString(R.string.recently_added) + "Most liked" -> context.getString(R.string.most_liked) + "conferences" -> context.getString(R.string.conferences) + "recent" -> context.getString(R.string.recent) + "live" -> context.getString(R.string.duration_live) + "Featured" -> context.getString(R.string.featured) + "Radio" -> context.getString(R.string.radio) + "trending_gaming" -> context.getString(R.string.trending_gaming) + "trending_music" -> context.getString(R.string.trending_music) + "trending_movies_and_shows" -> context.getString(R.string.trending_movies) + "trending_podcasts_episodes" -> context.getString(R.string.trending_podcasts) else -> kioskId } } diff --git a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java deleted file mode 100644 index da97179b6..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.schabi.newpipe.util.image; - -import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN; -import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.extractor.Image; - -import java.util.Comparator; -import java.util.List; - -public final class ImageStrategy { - - // when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred - // image quality is to these values (H stands for "Height") - private static final int BEST_LOW_H = 75; - private static final int BEST_MEDIUM_H = 250; - - private static PreferredImageQuality preferredImageQuality = PreferredImageQuality.MEDIUM; - - private ImageStrategy() { - } - - public static void setPreferredImageQuality(final PreferredImageQuality preferredImageQuality) { - ImageStrategy.preferredImageQuality = preferredImageQuality; - } - - public static boolean shouldLoadImages() { - return preferredImageQuality != PreferredImageQuality.NONE; - } - - - static double estimatePixelCount(final Image image, final double widthOverHeight) { - if (image.getHeight() == HEIGHT_UNKNOWN) { - if (image.getWidth() == WIDTH_UNKNOWN) { - // images whose size is completely unknown will be in their own subgroups, so - // any one of them will do, hence returning the same value for all of them - return 0; - } else { - return image.getWidth() * image.getWidth() / widthOverHeight; - } - } else if (image.getWidth() == WIDTH_UNKNOWN) { - return image.getHeight() * image.getHeight() * widthOverHeight; - } else { - return image.getHeight() * image.getWidth(); - } - } - - /** - * {@link #choosePreferredImage(List)} contains the description for this function's logic. - * - * @param images the images from which to choose - * @param nonNoneQuality the preferred quality (must NOT be {@link PreferredImageQuality#NONE}) - * @return the chosen preferred image, or {@link null} if the list is empty - * @see #choosePreferredImage(List) - */ - @Nullable - static String choosePreferredImage(@NonNull final List images, - final PreferredImageQuality nonNoneQuality) { - // this will be used to estimate the pixel count for images where only one of height or - // width are known - final double widthOverHeight = images.stream() - .filter(image -> image.getHeight() != HEIGHT_UNKNOWN - && image.getWidth() != WIDTH_UNKNOWN) - .mapToDouble(image -> ((double) image.getWidth()) / image.getHeight()) - .findFirst() - .orElse(1.0); - - final Image.ResolutionLevel preferredLevel = nonNoneQuality.toResolutionLevel(); - final Comparator initialComparator = Comparator - // the first step splits the images into groups of resolution levels - .comparingInt(i -> { - if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) { - return 3; // avoid unknowns as much as possible - } else if (i.getEstimatedResolutionLevel() == preferredLevel) { - return 0; // prefer a matching resolution level - } else if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.MEDIUM) { - return 1; // the preferredLevel is only 1 "step" away (either HIGH or LOW) - } else { - return 2; // the preferredLevel is the furthest away possible (2 "steps") - } - }) - // then each level's group is further split into two subgroups, one with known image - // size (which is also the preferred subgroup) and the other without - .thenComparing(image -> - image.getHeight() == HEIGHT_UNKNOWN && image.getWidth() == WIDTH_UNKNOWN); - - // The third step chooses, within each subgroup with known image size, the best image based - // on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups - // without known image size will be left untouched since estimatePixelCount always returns - // the same number for those. - final Comparator finalComparator = switch (nonNoneQuality) { - case NONE -> initialComparator; // unreachable - case LOW -> initialComparator.thenComparingDouble(image -> { - final double pixelCount = estimatePixelCount(image, widthOverHeight); - return Math.abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight); - }); - case MEDIUM -> initialComparator.thenComparingDouble(image -> { - final double pixelCount = estimatePixelCount(image, widthOverHeight); - return Math.abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight); - }); - case HIGH -> initialComparator.thenComparingDouble( - // this is reversed with a - so that the highest resolution is chosen - i -> -estimatePixelCount(i, widthOverHeight)); - }; - - return images.stream() - // using "min" basically means "take the first group, then take the first subgroup, - // then choose the best image, while ignoring all other groups and subgroups" - .min(finalComparator) - .map(Image::getUrl) - .orElse(null); - } - - /** - * Chooses an image amongst the provided list based on the user preference previously set with - * {@link #setPreferredImageQuality(PreferredImageQuality)}. {@code null} will be returned in - * case the list is empty or the user preference is to not show images. - *
- * These properties will be preferred, from most to least important: - *
    - *
  1. The image's {@link Image#getEstimatedResolutionLevel()} is not unknown and is close - * to {@link #preferredImageQuality}
  2. - *
  3. At least one of the image's width or height are known
  4. - *
  5. The highest resolution image is finally chosen if the user's preference is {@link - * PreferredImageQuality#HIGH}, otherwise the chosen image is the one that has the height - * closest to {@link #BEST_LOW_H} or {@link #BEST_MEDIUM_H}
  6. - *
- *
- * Use {@link #imageListToDbUrl(List)} if the URL is going to be saved to the database, to avoid - * saving nothing in case at the moment of saving the user preference is to not show images. - * - * @param images the images from which to choose - * @return the chosen preferred image, or {@link null} if the list is empty or the user disabled - * images - * @see #imageListToDbUrl(List) - */ - @Nullable - public static String choosePreferredImage(@NonNull final List images) { - if (preferredImageQuality == PreferredImageQuality.NONE) { - return null; // do not load images - } - - return choosePreferredImage(images, preferredImageQuality); - } - - /** - * Like {@link #choosePreferredImage(List)}, except that if {@link #preferredImageQuality} is - * {@link PreferredImageQuality#NONE} an image will be chosen anyway (with preferred quality - * {@link PreferredImageQuality#MEDIUM}. - *
- * To go back to a list of images (obviously with just the one chosen image) from a URL saved in - * the database use {@link #dbUrlToImageList(String)}. - * - * @param images the images from which to choose - * @return the chosen preferred image, or {@link null} if the list is empty - * @see #choosePreferredImage(List) - * @see #dbUrlToImageList(String) - */ - @Nullable - public static String imageListToDbUrl(@NonNull final List images) { - final PreferredImageQuality quality; - if (preferredImageQuality == PreferredImageQuality.NONE) { - quality = PreferredImageQuality.MEDIUM; - } else { - quality = preferredImageQuality; - } - - return choosePreferredImage(images, quality); - } - - /** - * Wraps the URL (coming from the database) in a {@code List} so that it is usable - * seamlessly in all of the places where the extractor would return a list of images, including - * allowing to build info objects based on database objects. - *
- * To obtain a url to save to the database from a list of images use {@link - * #imageListToDbUrl(List)}. - * - * @param url the URL to wrap coming from the database, or {@code null} to get an empty list - * @return a list containing just one {@link Image} wrapping the provided URL, with unknown - * image size fields, or an empty list if the URL is {@code null} - * @see #imageListToDbUrl(List) - */ - @NonNull - public static List dbUrlToImageList(@Nullable final String url) { - if (url == null) { - return List.of(); - } else { - return List.of(new Image(url, -1, -1, Image.ResolutionLevel.UNKNOWN)); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt new file mode 100644 index 000000000..aa59b4d0a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util.image + +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.Image.ResolutionLevel +import kotlin.math.abs + +object ImageStrategy { + // when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred + // image quality is to these values (H stands for "Height") + private const val BEST_LOW_H = 75 + private const val BEST_MEDIUM_H = 250 + + private var preferredImageQuality = PreferredImageQuality.MEDIUM + + @JvmStatic + fun setPreferredImageQuality(preferredImageQuality: PreferredImageQuality) { + ImageStrategy.preferredImageQuality = preferredImageQuality + } + + @JvmStatic + fun shouldLoadImages(): Boolean { + return preferredImageQuality != PreferredImageQuality.NONE + } + + @JvmStatic + fun estimatePixelCount(image: Image, widthOverHeight: Double): Double { + if (image.height == Image.HEIGHT_UNKNOWN) { + if (image.width == Image.WIDTH_UNKNOWN) { + // images whose size is completely unknown will be in their own subgroups, so + // any one of them will do, hence returning the same value for all of them + return 0.0 + } else { + return image.width * image.width / widthOverHeight + } + } else if (image.width == Image.WIDTH_UNKNOWN) { + return image.height * image.height * widthOverHeight + } else { + return (image.height * image.width).toDouble() + } + } + + /** + * [choosePreferredImage] contains the description for this function's logic. + * + * @param images the images from which to choose + * @param nonNoneQuality the preferred quality (must NOT be [PreferredImageQuality.NONE]) + * @return the chosen preferred image, or `null` if the list is empty + * @see [choosePreferredImage] + */ + @JvmStatic + fun choosePreferredImage(images: List, nonNoneQuality: PreferredImageQuality): String? { + // this will be used to estimate the pixel count for images where only one of height or + // width are known + val widthOverHeight = images + .filter { image -> + image.height != Image.HEIGHT_UNKNOWN && image.width != Image.WIDTH_UNKNOWN + } + .map { image -> (image.width.toDouble()) / image.height } + .elementAtOrNull(0) ?: 1.0 + + val preferredLevel = nonNoneQuality.toResolutionLevel() + // TODO: rewrite using kotlin collections API `groupBy` will be handy + val initialComparator = + Comparator // the first step splits the images into groups of resolution levels + .comparingInt { i: Image -> + return@comparingInt when (i.estimatedResolutionLevel) { + // avoid unknowns as much as possible + ResolutionLevel.UNKNOWN -> 3 + + // prefer a matching resolution level + preferredLevel -> 0 + + // the preferredLevel is only 1 "step" away (either HIGH or LOW) + ResolutionLevel.MEDIUM -> 1 + + // the preferredLevel is the furthest away possible (2 "steps") + else -> 2 + } + } + // then each level's group is further split into two subgroups, one with known image + // size (which is also the preferred subgroup) and the other without + .thenComparing { image -> image.height == Image.HEIGHT_UNKNOWN && image.width == Image.WIDTH_UNKNOWN } + + // The third step chooses, within each subgroup with known image size, the best image based + // on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups + // without known image size will be left untouched since estimatePixelCount always returns + // the same number for those. + val finalComparator = when (nonNoneQuality) { + PreferredImageQuality.NONE -> initialComparator + PreferredImageQuality.LOW -> initialComparator.thenComparingDouble { image -> + val pixelCount = estimatePixelCount(image, widthOverHeight) + abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight) + } + + PreferredImageQuality.MEDIUM -> initialComparator.thenComparingDouble { image -> + val pixelCount = estimatePixelCount(image, widthOverHeight) + abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight) + } + + PreferredImageQuality.HIGH -> initialComparator.thenComparingDouble { image -> + // this is reversed with a - so that the highest resolution is chosen + -estimatePixelCount(image, widthOverHeight) + } + } + + return images.stream() // using "min" basically means "take the first group, then take the first subgroup, + // then choose the best image, while ignoring all other groups and subgroups" + .min(finalComparator) + .map(Image::getUrl) + .orElse(null) + } + + /** + * Chooses an image amongst the provided list based on the user preference previously set with + * [setPreferredImageQuality]. `null` will be returned in + * case the list is empty or the user preference is to not show images. + *
+ * These properties will be preferred, from most to least important: + * + * 1. The image's [Image.estimatedResolutionLevel] is not unknown and is close to [preferredImageQuality] + * 2. At least one of the image's width or height are known + * 3. The highest resolution image is finally chosen if the user's preference is + * [PreferredImageQuality.HIGH], otherwise the chosen image is the one that has the height + * closest to [BEST_LOW_H] or [BEST_MEDIUM_H] + * + *
+ * Use [imageListToDbUrl] if the URL is going to be saved to the database, to avoid + * saving nothing in case at the moment of saving the user preference is to not show images. + * + * @param images the images from which to choose + * @return the chosen preferred image, or `null` if the list is empty or the user disabled + * images + * @see [imageListToDbUrl] + */ + @JvmStatic + fun choosePreferredImage(images: List): String? { + if (preferredImageQuality == PreferredImageQuality.NONE) { + return null // do not load images + } + + return choosePreferredImage(images, preferredImageQuality) + } + + /** + * Like [choosePreferredImage], except that if [preferredImageQuality] is + * [PreferredImageQuality.NONE] an image will be chosen anyway (with preferred quality + * [PreferredImageQuality.MEDIUM]. + *

+ * To go back to a list of images (obviously with just the one chosen image) from a URL saved in + * the database use [dbUrlToImageList]. + * + * @param images the images from which to choose + * @return the chosen preferred image, or `null` if the list is empty + * @see [choosePreferredImage] + * @see [dbUrlToImageList] + */ + @JvmStatic + fun imageListToDbUrl(images: List): String? { + val quality = when (preferredImageQuality) { + PreferredImageQuality.NONE -> PreferredImageQuality.MEDIUM + else -> preferredImageQuality + } + + return choosePreferredImage(images, quality) + } + + /** + * Wraps the URL (coming from the database) in a `List` so that it is usable + * seamlessly in all of the places where the extractor would return a list of images, including + * allowing to build info objects based on database objects. + *

+ * To obtain a url to save to the database from a list of images use [imageListToDbUrl]. + * + * @param url the URL to wrap coming from the database, or `null` to get an empty list + * @return a list containing just one [Image] wrapping the provided URL, with unknown + * image size fields, or an empty list if the URL is `null` + * @see [imageListToDbUrl] + */ + @JvmStatic + fun dbUrlToImageList(url: String?): List { + return when (url) { + null -> listOf() + else -> listOf(Image(url, -1, -1, ResolutionLevel.UNKNOWN)) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java deleted file mode 100644 index 7106359b3..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.schabi.newpipe.util.image; - -import android.content.Context; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.Image; - -public enum PreferredImageQuality { - NONE, - LOW, - MEDIUM, - HIGH; - - public static PreferredImageQuality fromPreferenceKey(final Context context, final String key) { - if (context.getString(R.string.image_quality_none_key).equals(key)) { - return NONE; - } else if (context.getString(R.string.image_quality_low_key).equals(key)) { - return LOW; - } else if (context.getString(R.string.image_quality_high_key).equals(key)) { - return HIGH; - } else { - return MEDIUM; // default to medium - } - } - - public Image.ResolutionLevel toResolutionLevel() { - switch (this) { - case LOW: - return Image.ResolutionLevel.LOW; - case MEDIUM: - return Image.ResolutionLevel.MEDIUM; - case HIGH: - return Image.ResolutionLevel.HIGH; - default: - case NONE: - return Image.ResolutionLevel.UNKNOWN; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt new file mode 100644 index 000000000..b90ba87aa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util.image + +import android.content.Context +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.Image.ResolutionLevel + +enum class PreferredImageQuality { + NONE, + LOW, + MEDIUM, + HIGH; + + fun toResolutionLevel(): ResolutionLevel { + return when (this) { + LOW -> ResolutionLevel.LOW + MEDIUM -> ResolutionLevel.MEDIUM + HIGH -> ResolutionLevel.HIGH + NONE -> ResolutionLevel.UNKNOWN + } + } + + companion object { + @JvmStatic + fun fromPreferenceKey(context: Context, key: String?): PreferredImageQuality { + return when (key) { + context.getString(R.string.image_quality_none_key) -> NONE + context.getString(R.string.image_quality_low_key) -> LOW + context.getString(R.string.image_quality_high_key) -> HIGH + else -> MEDIUM // default to medium + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java index be603f41a..b1357b943 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java @@ -54,30 +54,6 @@ public final class TimestampExtractor { return new TimestampMatchDTO(timestampStart, timestampEnd, seconds); } - public static class TimestampMatchDTO { - private final int timestampStart; - private final int timestampEnd; - private final int seconds; - - public TimestampMatchDTO( - final int timestampStart, - final int timestampEnd, - final int seconds) { - this.timestampStart = timestampStart; - this.timestampEnd = timestampEnd; - this.seconds = seconds; - } - - public int timestampStart() { - return timestampStart; - } - - public int timestampEnd() { - return timestampEnd; - } - - public int seconds() { - return seconds; - } + public record TimestampMatchDTO(int timestampStart, int timestampEnd, int seconds) { } } diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java deleted file mode 100644 index 35a9fd996..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.schabi.newpipe.util.text; - -import static org.schabi.newpipe.util.text.InternalUrlsHandler.playOnPopup; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.util.external_communication.ShareUtils; - -import io.reactivex.rxjava3.disposables.CompositeDisposable; - -final class TimestampLongPressClickableSpan extends LongPressClickableSpan { - - @NonNull - private final Context context; - @NonNull - private final String descriptionText; - @NonNull - private final CompositeDisposable disposables; - @NonNull - private final StreamingService relatedInfoService; - @NonNull - private final String relatedStreamUrl; - @NonNull - private final TimestampExtractor.TimestampMatchDTO timestampMatchDTO; - - TimestampLongPressClickableSpan( - @NonNull final Context context, - @NonNull final String descriptionText, - @NonNull final CompositeDisposable disposables, - @NonNull final StreamingService relatedInfoService, - @NonNull final String relatedStreamUrl, - @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { - this.context = context; - this.descriptionText = descriptionText; - this.disposables = disposables; - this.relatedInfoService = relatedInfoService; - this.relatedStreamUrl = relatedStreamUrl; - this.timestampMatchDTO = timestampMatchDTO; - } - - @Override - public void onClick(@NonNull final View view) { - playOnPopup(context, relatedStreamUrl, relatedInfoService, - timestampMatchDTO.seconds()); - } - - @Override - public void onLongClick(@NonNull final View view) { - ShareUtils.copyToClipboard(context, getTimestampTextToCopy( - relatedInfoService, relatedStreamUrl, descriptionText, timestampMatchDTO)); - } - - @NonNull - private static String getTimestampTextToCopy( - @NonNull final StreamingService relatedInfoService, - @NonNull final String relatedStreamUrl, - @NonNull final String descriptionText, - @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { - // TODO: use extractor methods to get timestamps when this feature will be implemented in it - if (relatedInfoService == ServiceList.YouTube) { - return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds(); - } else if (relatedInfoService == ServiceList.SoundCloud - || relatedInfoService == ServiceList.MediaCCC) { - return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds(); - } else if (relatedInfoService == ServiceList.PeerTube) { - return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds(); - } - - // Return timestamp text for other services - return descriptionText.subSequence(timestampMatchDTO.timestampStart(), - timestampMatchDTO.timestampEnd()).toString(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt new file mode 100644 index 000000000..a76c5c31a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util.text + +import android.content.Context +import android.view.View +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.schabi.newpipe.extractor.ServiceList +import org.schabi.newpipe.extractor.StreamingService +import org.schabi.newpipe.util.external_communication.ShareUtils +import org.schabi.newpipe.util.text.TimestampExtractor.TimestampMatchDTO + +class TimestampLongPressClickableSpan( + private val context: Context, + private val descriptionText: String, + private val disposables: CompositeDisposable, + private val relatedInfoService: StreamingService, + private val relatedStreamUrl: String, + private val timestampMatchDTO: TimestampMatchDTO +) : LongPressClickableSpan() { + override fun onClick(view: View) { + InternalUrlsHandler.playOnPopup( + context, + relatedStreamUrl, + relatedInfoService, + timestampMatchDTO.seconds() + ) + } + + override fun onLongClick(view: View) { + ShareUtils.copyToClipboard( + context, + getTimestampTextToCopy( + relatedInfoService, + relatedStreamUrl, + descriptionText, + timestampMatchDTO + ) + ) + } + + companion object { + private fun getTimestampTextToCopy( + relatedInfoService: StreamingService, + relatedStreamUrl: String, + descriptionText: String, + timestampMatchDTO: TimestampMatchDTO + ): String { + // TODO: use extractor methods to get timestamps when this feature will be implemented in it + when (relatedInfoService) { + ServiceList.YouTube -> + return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds() + ServiceList.SoundCloud, ServiceList.MediaCCC -> + return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds() + ServiceList.PeerTube -> + return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds() + } + + // Return timestamp text for other services + return descriptionText.substring( + timestampMatchDTO.timestampStart(), + timestampMatchDTO.timestampEnd() + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java index 175c81e46..7452fff09 100644 --- a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -35,12 +35,12 @@ public class ExpandableSurfaceView extends SurfaceView { && resizeMode != RESIZE_MODE_FIT && verticalVideo ? maxHeight : baseHeight; - if (height == 0) { + if (width == 0 || height == 0) { return; } final float viewAspectRatio = width / ((float) height); - final float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + final float aspectDeformation = (videoAspectRatio / viewAspectRatio) - 1; scaleX = 1.0f; scaleY = 1.0f; @@ -100,7 +100,7 @@ public class ExpandableSurfaceView extends SurfaceView { } public void setAspectRatio(final float aspectRatio) { - if (videoAspectRatio == aspectRatio) { + if (videoAspectRatio == aspectRatio || aspectRatio == 0 || !Float.isFinite(aspectRatio)) { return; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index eed5db463..1d2483e79 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -85,6 +85,7 @@ public class DownloadRunnableFallback extends Thread { if (mMission.unknownLength || mConn.getResponseCode() == 200) { // restart amount of bytes downloaded mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0]; + start = 0; // reset position to avoid writing at wrong offset } mF = mMission.storage.getStream(); diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index a0a7744f6..dfff3c2cc 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -117,5 +117,4 @@ Lenn ar video, pad: Titouroù: Video - diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 016266e31..b5e746a8b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -860,7 +860,7 @@ Compte fermé\n\n%1$s fournit la raison suivante : %2$s Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par l\'expiration de l\'URL de streaming ou une interdiction d\'IP Erreur HTTP %1$s reçue du serveur pendant la lecture - Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par une interdiction d\'IP ou des problèmes de désobfuscation d\'URL de streaming. + Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par un bannissement d\'IP ou des problèmes de désobfuscation de l\'URL de streaming %1$s a refusé de fournir des données et a demandé un identifiant pour confirmer que le demandeur n\'est pas un robot.\n\nVotre adresse IP a peut-être été temporairement bannie par %1$s. Vous pouvez patienter un peu ou changer d\'adresse IP (par exemple en activant/désactivant un VPN, ou en passant du Wi-Fi aux données mobiles). Ce contenu n\'est pas disponible pour le pays actuellement sélectionné.\n\nModifiez votre sélection dans « Paramètres > Contenu > Pays par défaut ». diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index b45a2e7c5..45973c900 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -826,4 +826,8 @@ ਅਜੇ ਤੱਕ ਕੋਈ ਫੀਡ ਗਰੁੱਪ ਨਹੀਂ ਬਣਾਇਆ ਗਿਆ ਚੈਨਲ ਗਰੁੱਪ ਪੰਨਾ ਪਸੰਦ + ਫ਼ਾਈਲ ਮਿਟਾਓ + ਐਂਟਰੀ ਮਿਟਾਓ + ਖ਼ਾਤਾ ਬੰਦ ਕੀਤਾ ਗਿਆ\n\n%1$s ਇਹ ਕਾਰਨ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ: %2$s + ਐਂਟਰੀ ਮਿਟਾ ਦਿੱਤੀ ਗਈ diff --git a/app/src/main/res/values-rom/strings.xml b/app/src/main/res/values-rom/strings.xml new file mode 100644 index 000000000..55344e519 --- /dev/null +++ b/app/src/main/res/values-rom/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt b/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt index d9be2271e..de90061a4 100644 --- a/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt +++ b/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.local.playlist import android.content.Context @@ -9,7 +14,6 @@ import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST -import java.util.stream.Stream class ExportPlaylistTest { @@ -41,9 +45,7 @@ class ExportPlaylistTest { */ val playlist = asPlaylist( - (10..70) - .map { id -> "https://www.youtube.com/watch?v=aaaaaaaaa$id" } // YouTube video IDs are 11 characters long - .stream() + (10..70).map { id -> "https://www.youtube.com/watch?v=aaaaaaaaa$id" } // YouTube video IDs are 11 characters long ) val url = export(YOUTUBE_TEMP_PLAYLIST, playlist, mock(Context::class.java)) @@ -78,13 +80,11 @@ class ExportPlaylistTest { } fun asPlaylist(vararg urls: String): List { - return asPlaylist(Stream.of(*urls)) + return asPlaylist(listOf(*urls)) } -fun asPlaylist(urls: Stream): List { - return urls - .map { url: String -> newPlaylistStreamEntry(url) } - .toList() +fun asPlaylist(urls: List): List { + return urls.map { newPlaylistStreamEntry(it) } } fun newPlaylistStreamEntry(url: String): PlaylistStreamEntry { diff --git a/fastlane/metadata/android/ko/changelogs/1006.txt b/fastlane/metadata/android/ko/changelogs/1006.txt new file mode 100644 index 000000000..1729e9827 --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/1006.txt @@ -0,0 +1,16 @@ +# 개선됨 +타임스탬프를 클릭할 때 현재 플레이어를 유지합니다. +가능한 경우 보류 중인 다운로드 미션을 복구하세요. +파일 삭제 없이 다운로드를 삭제하는 옵션을 추가하세요. +오버레이 권한: Android > R에 대한 설명 대화 상자 표시 +사운드클라우드 링크 열기 지원 +많은 작은 개선과 최적화 + +# 고정 +7 이하의 안드로이드 버전에 대한 짧은 숫자 형식을 수정하세요. +고스트 알림 수정 +SRT 자막 파일 수정 +고정된 수많은 충돌 사고 + +# 개발 +내부 코드 현대화 diff --git a/fastlane/metadata/android/sv/changelogs/1006.txt b/fastlane/metadata/android/sv/changelogs/1006.txt new file mode 100644 index 000000000..90825e8a8 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/1006.txt @@ -0,0 +1,16 @@ +# Förbättrat +Behåll aktuell spelare när du klickar på tidsstämplar +Försök att återställa väntande nedladdningsuppdrag när det är möjligt +Lägg till alternativ för att ta bort en nedladdning utan att också ta bort filen +Överläggsbehörighet: visa förklarande dialogruta för Android > R +Stöd för att öppna on.soundcloud-länkar +Många små förbättringar och optimeringar + +# Åtgärdat +Åtgärdade formatering av korta antal för Android-versioner under 7 +Åtgärdade Ghost Notifications +Åtgärdade för SRT-undertextfiler +Åtgärdade massor av krascher + +# Utveckling +Intern kodmodernisering diff --git a/fastlane/metadata/android/uk/changelogs/1006.txt b/fastlane/metadata/android/uk/changelogs/1006.txt new file mode 100644 index 000000000..6534e9145 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/1006.txt @@ -0,0 +1,16 @@ +# Покращено +Зберігати поточний програвач при натисканні на часові позначки +Намагатися відновлювати місії, що очікують завантаження, коли це можливо +Додано опцію видалення завантаження без одночасного видалення файлу +Дозвіл на накладання: відображення пояснювального діалогового вікна для Android > R +Підтримка відкриття посилання на .soundcloud +Багато дрібних покращень та оптимізацій + +# Виправлено +Виправлено форматування короткого лічильника для версій Android нижче 7 +Виправлено сповіщення-примари +Виправлення для файлів субтитрів SRT +Виправлено безліч збоїв + +# Розробка +Модернізація внутрішнього коду diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b345d54b4..efa55488d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ lazy-column-scrollbar = "2.2.0" leakcanary = "2.14" lifecycle = "2.9.4" # Newer versions require minSdk >= 23 markwon = "4.6.2" -material = "1.13.0" +material = "1.11.0" # TODO: update to newer version after bug is fixed. See https://github.com/TeamNewPipe/NewPipe/pull/13018 media = "1.7.1" mockitoCore = "5.21.0" navigation-compose = "2.8.3"