Merge pull request #12978 from dustdfg/kotlin_merged
Conversion to kotlin of multiple files
This commit is contained in:
commit
2fc5412ceb
39 changed files with 677 additions and 963 deletions
|
|
@ -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.
|
* The user actions that can cause an error.
|
||||||
*/
|
*/
|
||||||
public enum UserAction {
|
enum class UserAction(val message: String) {
|
||||||
USER_REPORT("user report"),
|
USER_REPORT("user report"),
|
||||||
UI_ERROR("ui error"),
|
UI_ERROR("ui error"),
|
||||||
DATABASE_IMPORT_EXPORT("database import or export"),
|
DATABASE_IMPORT_EXPORT("database import or export"),
|
||||||
|
|
@ -36,14 +41,4 @@ public enum UserAction {
|
||||||
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
|
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
|
||||||
PLAY_ON_POPUP("play on popup"),
|
PLAY_ON_POPUP("play on popup"),
|
||||||
SUBSCRIPTIONS("loading subscriptions");
|
SUBSCRIPTIONS("loading subscriptions");
|
||||||
|
|
||||||
private final String message;
|
|
||||||
|
|
||||||
UserAction(final String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 + "]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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]"
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
* Item view mode for streams & playlist listing screens.
|
||||||
*/
|
*/
|
||||||
public enum ItemViewMode {
|
enum class ItemViewMode {
|
||||||
/**
|
/**
|
||||||
* Default mode.
|
* Default mode.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package org.schabi.newpipe.local.playlist;
|
|
||||||
|
|
||||||
public enum PlayListShareMode {
|
|
||||||
|
|
||||||
JUST_URLS,
|
|
||||||
WITH_TITLES,
|
|
||||||
YOUTUBE_TEMP_PLAYLIST
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
public enum PlayerType {
|
|
||||||
MAIN,
|
|
||||||
AUDIO,
|
|
||||||
POPUP;
|
|
||||||
}
|
|
||||||
12
app/src/main/java/org/schabi/newpipe/player/PlayerType.kt
Normal file
12
app/src/main/java/org/schabi/newpipe/player/PlayerType.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -17,10 +17,10 @@ import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
|
||||||
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
|
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,14 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.InitEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RecoveryEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,11 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent;
|
||||||
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
|
||||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package org.schabi.newpipe.player.playqueue.events;
|
|
||||||
|
|
||||||
public class InitEvent implements PlayQueueEvent {
|
|
||||||
@Override
|
|
||||||
public PlayQueueEventType type() {
|
|
||||||
return PlayQueueEventType.INIT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package org.schabi.newpipe.player.playqueue.events;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
public interface PlayQueueEvent extends Serializable {
|
|
||||||
PlayQueueEventType type();
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package org.schabi.newpipe.settings.preferencesearch;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public interface PreferenceSearchResultListener {
|
|
||||||
void onSearchResultClicked(@NonNull PreferenceSearchItem result);
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt
Normal file
64
app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
191
app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt
Normal file
191
app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,30 +54,6 @@ public final class TimestampExtractor {
|
||||||
return new TimestampMatchDTO(timestampStart, timestampEnd, seconds);
|
return new TimestampMatchDTO(timestampStart, timestampEnd, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class TimestampMatchDTO {
|
public record TimestampMatchDTO(int timestampStart, int timestampEnd, int seconds) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue