Merge pull request #12978 from dustdfg/kotlin_merged

Conversion to kotlin of multiple files
This commit is contained in:
Aayush Gupta 2026-01-02 15:46:29 +08:00 committed by GitHub
commit 7283701073
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 677 additions and 963 deletions

View file

@ -1,9 +1,14 @@
package org.schabi.newpipe.error;
/*
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
* 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;
}
}

View file

@ -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 + "]";
}
}

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
* 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]"
}

View file

@ -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<SuggestionItem, SuggestionListAdapter.SuggestionItemHolder> {
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<SuggestionItem> {
@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
}
}
}

View file

@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
* 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<SuggestionItem, SuggestionItemHolder>(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<SuggestionItem>() {
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
}
}
}

View file

@ -1,9 +1,14 @@
package org.schabi.newpipe.info_list;
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* 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.
*/

View file

@ -1,8 +0,0 @@
package org.schabi.newpipe.local.playlist;
public enum PlayListShareMode {
JUST_URLS,
WITH_TITLES,
YOUTUBE_TEMP_PLAYLIST
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.local.playlist
enum class PlayListShareMode {
JUST_URLS,
WITH_TITLES,
YOUTUBE_TEMP_PLAYLIST
}

View file

@ -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<List<PlaylistRemoteEntity>> getPlaylists() {
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
}
public Flowable<PlaylistRemoteEntity> getPlaylist(final long playlistId) {
return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl())
.subscribeOn(Schedulers.io());
}
public Single<Integer> deletePlaylist(final long playlistId) {
return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId))
.subscribeOn(Schedulers.io());
}
public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
final List<Long> deletedItems) {
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
for (final Long uid: deletedItems) {
playlistRemoteTable.deletePlaylist(uid);
}
for (final PlaylistRemoteEntity item: updateItems) {
playlistRemoteTable.upsert(item);
}
})).subscribeOn(Schedulers.io());
}
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
return Single.fromCallable(() -> {
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
return playlistRemoteTable.upsert(playlist);
}).subscribeOn(Schedulers.io());
}
public Single<Integer> 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());
}
}

View file

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
* 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<MutableList<PlaylistRemoteEntity>>
get() = playlistRemoteTable.playlists.subscribeOn(Schedulers.io())
fun getPlaylist(playlistId: Long): Flowable<PlaylistRemoteEntity> {
return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io())
}
fun getPlaylist(info: PlaylistInfo): Flowable<MutableList<PlaylistRemoteEntity>> {
return playlistRemoteTable.getPlaylist(info.serviceId.toLong(), info.url)
.subscribeOn(Schedulers.io())
}
fun deletePlaylist(playlistId: Long): Single<Int> {
return Single.fromCallable { playlistRemoteTable.deletePlaylist(playlistId) }
.subscribeOn(Schedulers.io())
}
fun updatePlaylists(
updateItems: List<PlaylistRemoteEntity>,
deletedItems: List<Long>
): Completable {
return Completable.fromRunnable {
database.runInTransaction {
deletedItems.forEach { playlistRemoteTable.deletePlaylist(it) }
updateItems.forEach { playlistRemoteTable.upsert(it) }
}
}.subscribeOn(Schedulers.io())
}
fun onBookmark(playlistInfo: PlaylistInfo): Single<Long> {
return Single.fromCallable {
val playlist = PlaylistRemoteEntity(playlistInfo)
playlistRemoteTable.upsert(playlist)
}.subscribeOn(Schedulers.io())
}
fun onUpdate(playlistId: Long, playlistInfo: PlaylistInfo): Single<Int> {
return Single.fromCallable {
val playlist = PlaylistRemoteEntity(playlistInfo).apply { uid = playlistId }
playlistRemoteTable.update(playlist)
}.subscribeOn(Schedulers.io())
}
}

View file

@ -1,7 +0,0 @@
package org.schabi.newpipe.player;
public enum PlayerType {
MAIN,
AUDIO,
POPUP;
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.player
enum class PlayerType {
MAIN,
AUDIO,
POPUP
}

View file

@ -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;

View file

@ -4,15 +4,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.MainActivity;
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.ArrayList;

View file

@ -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;

View file

@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2017-2026 NewPipe contributors <https://newpipe.net>
* 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 }
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -1,8 +0,0 @@
package org.schabi.newpipe.player.playqueue.events;
public class InitEvent implements PlayQueueEvent {
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.INIT;
}
}

View file

@ -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;
}
}

View file

@ -1,7 +0,0 @@
package org.schabi.newpipe.player.playqueue.events;
import java.io.Serializable;
public interface PlayQueueEvent extends Serializable {
PlayQueueEventType type();
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<String> getAllRelevantSearchFields() {
return List.of(getTitle(), getSummary(), getEntries(), getBreadcrumbs());
}
@NonNull
@Override
public String toString() {
return "PreferenceItem: " + title + " " + summary + " " + key;
}
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022-2025 NewPipe contributors <https://newpipe.net>
* 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<String?> {
return mutableListOf(title, summary, entries, breadcrumbs)
}
override fun toString(): String {
return "PreferenceItem: $title $summary $key"
}
}

View file

@ -1,7 +0,0 @@
package org.schabi.newpipe.settings.preferencesearch;
import androidx.annotation.NonNull;
public interface PreferenceSearchResultListener {
void onSearchResultClicked(@NonNull PreferenceSearchItem result);
}

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2022-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.preferencesearch
interface PreferenceSearchResultListener {
fun onSearchResultClicked(result: PreferenceSearchItem)
}

View file

@ -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);
}
}

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
* 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)
}
}

View file

@ -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<Image> 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<Image> initialComparator = Comparator
// the first step splits the images into groups of resolution levels
.<Image>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<Image> 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.
* <br>
* These properties will be preferred, from most to least important:
* <ol>
* <li>The image's {@link Image#getEstimatedResolutionLevel()} is not unknown and is close
* to {@link #preferredImageQuality}</li>
* <li>At least one of the image's width or height are known</li>
* <li>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}</li>
* </ol>
* <br>
* 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<Image> 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}.
* <br>
* 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<Image> 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<Image>} 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.
* <br>
* 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<Image> dbUrlToImageList(@Nullable final String url) {
if (url == null) {
return List.of();
} else {
return List.of(new Image(url, -1, -1, Image.ResolutionLevel.UNKNOWN));
}
}
}

View file

@ -0,0 +1,191 @@
/*
* SPDX-FileCopyrightText: 2023-2025 NewPipe contributors <https://newpipe.net>
* 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<Image>, 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.
* <br>
* 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]
*
* <br>
* 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<Image>): 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].
* <br></br>
* 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<Image>): 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<Image>` 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.
* <br></br>
* 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<Image> {
return when (url) {
null -> listOf()
else -> listOf(Image(url, -1, -1, ResolutionLevel.UNKNOWN))
}
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2023-2025 NewPipe contributors <https://newpipe.net>
* 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
}
}
}
}

View file

@ -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) {
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2023-2025 NewPipe contributors <https://newpipe.net>
* 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()
)
}
}
}