This commit is contained in:
Denis Çerri 2023-01-08 16:50:57 +01:00
parent 51ff22abcd
commit b330a5ba08
No known key found for this signature in database
GPG key ID: 96B3554AF5B193EE
30 changed files with 1016 additions and 762 deletions

View file

@ -113,7 +113,7 @@ dependencies {
implementation "com.devbrackets.android:exomedia:4.3.0" implementation "com.devbrackets.android:exomedia:4.3.0"
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("com.squareup.picasso:picasso:2.8") implementation("com.squareup.picasso:picasso:2.71828")
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
@ -131,4 +131,10 @@ dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer"
implementation "com.squareup.retrofit2:retrofit:$retrofitVer"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVer"
implementation "org.kodein.di:kodein-di-generic-jvm:6.4.0"
implementation "org.kodein.di:kodein-di-framework-android-x:$kodeinVer"
} }

View file

@ -39,7 +39,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".page.settings.SettingsActivity" android:name=".ui.settings.SettingsActivity"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@ -48,7 +48,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".page.CustomCommandActivity" android:name=".ui.CustomCommandActivity"
android:theme="@style/AppDefaultTheme" android:theme="@style/AppDefaultTheme"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:label="@string/run_command" android:label="@string/run_command"

View file

@ -5,13 +5,10 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.SystemClock import android.os.SystemClock
import android.provider.DocumentsContract
import android.provider.Settings.Global
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
@ -19,7 +16,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.database.Video import com.deniscerri.ytdlnis.database.Video
import com.deniscerri.ytdlnis.page.CustomCommandActivity import com.deniscerri.ytdlnis.ui.CustomCommandActivity
import com.deniscerri.ytdlnis.service.DownloadInfo import com.deniscerri.ytdlnis.service.DownloadInfo
import com.deniscerri.ytdlnis.service.IDownloaderListener import com.deniscerri.ytdlnis.service.IDownloaderListener
import com.deniscerri.ytdlnis.service.IDownloaderService import com.deniscerri.ytdlnis.service.IDownloaderService
@ -29,7 +26,6 @@ import com.deniscerri.ytdlnis.work.FileTransferWorker
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.originDir import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.originDir
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.title import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.title
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.downLocation import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.downLocation
import com.yausername.youtubedl_android.DownloadProgressCallback
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import com.yausername.youtubedl_android.YoutubeDLResponse import com.yausername.youtubedl_android.YoutubeDLResponse
@ -37,13 +33,11 @@ import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.jvm.functions.Function3
class DownloaderService : Service() { class DownloaderService : Service() {

View file

@ -21,10 +21,10 @@ import com.deniscerri.ytdlnis.DownloaderService.LocalBinder
import com.deniscerri.ytdlnis.database.DatabaseManager import com.deniscerri.ytdlnis.database.DatabaseManager
import com.deniscerri.ytdlnis.database.Video import com.deniscerri.ytdlnis.database.Video
import com.deniscerri.ytdlnis.databinding.ActivityMainBinding import com.deniscerri.ytdlnis.databinding.ActivityMainBinding
import com.deniscerri.ytdlnis.page.DownloadsFragment import com.deniscerri.ytdlnis.ui.DownloadsFragment
import com.deniscerri.ytdlnis.page.HomeFragment import com.deniscerri.ytdlnis.ui.HomeFragment
import com.deniscerri.ytdlnis.page.MoreFragment import com.deniscerri.ytdlnis.ui.MoreFragment
import com.deniscerri.ytdlnis.page.settings.SettingsActivity import com.deniscerri.ytdlnis.ui.settings.SettingsActivity
import com.deniscerri.ytdlnis.service.IDownloaderListener import com.deniscerri.ytdlnis.service.IDownloaderListener
import com.deniscerri.ytdlnis.service.IDownloaderService import com.deniscerri.ytdlnis.service.IDownloaderService
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil

View file

@ -0,0 +1,42 @@
package com.deniscerri.ytdlnis.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.HistoryDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ResultItem
@Database(
entities = [ResultItem::class, HistoryItem::class, DownloadItem::class],
version = 1,
autoMigrations = []
)
abstract class DBManager : RoomDatabase(){
abstract val resultDao : ResultDao
abstract val historyDao : HistoryDao
abstract val downloadDao : DownloadDao
companion object {
//prevents multiple instances of db getting created at the same time
@Volatile
private var instance : DBManager? = null
//if its not null return it, otherwise create db
fun getInstance(context: Context) : DBManager {
return instance ?: synchronized(this){
val dbInstance = Room.databaseBuilder(
context.applicationContext,
DBManager::class.java,
"YTDLnisDatabase"
).build()
instance = dbInstance
dbInstance
}
}
}
}

View file

@ -0,0 +1,29 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil
@Dao
interface DownloadDao {
@Query("SELECT * FROM downloads")
fun getAllDownloads() : LiveData<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
suspend fun getDownloadById(id: Int) : DownloadItem
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: DownloadItem)
@Query("DELETE FROM downloads")
suspend fun deleteAll()
@Delete
suspend fun delete(item: DownloadItem)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: DownloadItem)
}

View file

@ -0,0 +1,37 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil
@Dao
interface HistoryDao {
@Query("SELECT * FROM history WHERE title LIKE '%'||:query||'%' AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN id END ASC," +
"CASE WHEN :sort = 'DESC' THEN id END DESC," +
"CASE WHEN :sort = '' THEN id END DESC ")
suspend fun getHistory(query : String, format : String, site : String, sort : String) : LiveData<List<HistoryItem>>
@Query("SELECT * FROM history")
fun getAllHistory() : LiveData<List<HistoryItem>>
@Query("SELECT * FROM history WHERE id=:id LIMIT 1")
suspend fun getHistoryItem(id: Int) : HistoryItem
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: HistoryItem)
@Delete
suspend fun delete(item: HistoryItem)
@Query("DELETE FROM history")
suspend fun deleteAll()
@Query("DELETE FROM history WHERE id > (SELECT MIN(h.id) FROM history h WHERE h.url = history.url AND h.type = history.type)")
suspend fun deleteDuplicates()
@Update
suspend fun update(item: HistoryItem)
}

View file

@ -0,0 +1,21 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.FileUtil
@Dao
interface ResultDao {
@Query("SELECT * FROM results")
fun getResults() : LiveData<List<ResultItem>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: ResultItem)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ResultItem)
@Query("DELETE FROM results")
suspend fun deleteAll()
}

View file

@ -0,0 +1,18 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "downloads")
data class DownloadItem(
@PrimaryKey(autoGenerate = true)
val id: Int,
val url: String,
val title: String,
val author: String,
val duration: String,
val thumb: String,
val type: String,
val downloadPath: String,
val website: String
)

View file

@ -0,0 +1,20 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "history")
data class HistoryItem(
@PrimaryKey(autoGenerate = true)
val id: Int,
val url: String,
val title: String,
val author: String,
val duration: String,
val thumb: String,
val type: String,
val time: String,
val downloadPath: String,
val website: String
//val isQueuedDownload: Int
)

View file

@ -0,0 +1,17 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "results")
data class ResultItem(
@PrimaryKey(autoGenerate = true)
val id: Int,
val url: String,
val title: String,
val author: String,
val duration: String,
val thumb: String,
val website: String,
val playlistTitle: String
)

View file

@ -0,0 +1,23 @@
package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
class DownloadRepository(private val downloadDao: DownloadDao) {
val allDownloads : LiveData<List<DownloadItem>> = downloadDao.getAllDownloads()
suspend fun insert(item: DownloadItem){
downloadDao.insert(item)
}
suspend fun delete(item: DownloadItem){
downloadDao.delete(item)
}
suspend fun update(item: DownloadItem){
downloadDao.update(item)
}
}

View file

@ -0,0 +1,50 @@
package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData
import androidx.room.Transaction
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.HistoryDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil
class HistoryRepository(private val historyDao: HistoryDao) {
val allHistory : LiveData<List<HistoryItem>> = historyDao.getAllHistory()
suspend fun getItem(id: Int) : HistoryItem?{
return historyDao.getHistoryItem(id)
}
suspend fun insert(item: HistoryItem){
historyDao.insert(item)
}
suspend fun delete(item: HistoryItem){
historyDao.delete(item)
}
suspend fun deleteAll(){
historyDao.deleteAll()
}
suspend fun deleteDuplicates(){
historyDao.deleteDuplicates()
}
suspend fun update(item: HistoryItem){
historyDao.update(item)
}
suspend fun clearDeletedHistory(){
val fileUtil = FileUtil()
allHistory.value?.forEach { item ->
if (!fileUtil.exists(item.downloadPath)){
delete(item)
}
}
}
suspend fun checkDownloaded(){
}
}

View file

@ -0,0 +1,27 @@
package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
class ResultRepository(private val resultDao: ResultDao) {
val allResults : LiveData<List<ResultItem>> = resultDao.getResults()
suspend fun insert(items: List<ResultItem>){
items.forEach {
resultDao.insert(it)
}
}
suspend fun deleteAll(){
resultDao.deleteAll()
}
suspend fun update(item: ResultItem){
resultDao.update(item)
}
}

View file

@ -0,0 +1,34 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class DownloadViewModel(application: Application) : AndroidViewModel(application) {
private val repository : DownloadRepository
private val allDownloads : LiveData<List<DownloadItem>>
init {
val dao = DBManager.getInstance(application).downloadDao
repository = DownloadRepository(dao)
allDownloads = repository.allDownloads
}
fun insertDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
repository.insert(item)
}
fun deleteDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item)
}
fun updateDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
repository.update(item);
}
}

View file

@ -0,0 +1,48 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class HistoryViewModel(application: Application) : AndroidViewModel(application) {
private val repository : HistoryRepository
private val allHistory : LiveData<List<HistoryItem>>
init {
val dao = DBManager.getInstance(application).historyDao
repository = HistoryRepository(dao)
allHistory = repository.allHistory
}
fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){
repository.insert(item)
}
fun delete(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){
repository.delete(item)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
fun deleteDuplicates() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteDuplicates()
}
fun update(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){
repository.update(item);
}
fun clearDeleted() = viewModelScope.launch(Dispatchers.IO) {
repository.clearDeletedHistory()
}
}

View file

@ -0,0 +1,36 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ResultViewModel(application: Application) : AndroidViewModel(application) {
private val repository : ResultRepository
private val allResults : LiveData<List<ResultItem>>
init {
val dao = DBManager.getInstance(application).resultDao
repository = ResultRepository(dao)
allResults = repository.allResults
}
fun insert(items: List<ResultItem>) = viewModelScope.launch(Dispatchers.IO){
repository.insert(items)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
fun update(item: ResultItem) = viewModelScope.launch(Dispatchers.IO){
repository.update(item);
}
}

View file

@ -1,738 +0,0 @@
package com.deniscerri.ytdlnis.page;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Handler;
import android.os.Looper;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.deniscerri.ytdlnis.MainActivity;
import com.deniscerri.ytdlnis.R;
import com.deniscerri.ytdlnis.adapter.DownloadsRecyclerViewAdapter;
import com.deniscerri.ytdlnis.database.DatabaseManager;
import com.deniscerri.ytdlnis.database.Video;
import com.deniscerri.ytdlnis.service.DownloadInfo;
import com.deniscerri.ytdlnis.service.IDownloaderListener;
import com.deniscerri.ytdlnis.util.FileUtil;
import com.facebook.shimmer.ShimmerFrameLayout;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
/**
* A fragment representing a list of Items.
*/
public class DownloadsFragment extends Fragment implements DownloadsRecyclerViewAdapter.OnItemClickListener, View.OnClickListener, View.OnLongClickListener{
private boolean downloading = false;
private View fragmentView;
private DatabaseManager databaseManager;
Context context;
Activity activity;
MainActivity mainActivity;
Context fragmentContext;
private LayoutInflater layoutinflater;
private ShimmerFrameLayout shimmerCards;
private MaterialToolbar topAppBar;
private RecyclerView recyclerView;
private DownloadsRecyclerViewAdapter downloadsRecyclerViewAdapter;
private BottomSheetDialog bottomSheet;
private BottomSheetDialog sortSheet;
private Handler uiHandler;
private RelativeLayout no_results;
private LinearLayout selectionChips;
private ChipGroup websiteGroup;
private ArrayList<Video> downloadsObjects;
public ArrayList<Video> selectedObjects;
private LinearProgressIndicator progressBar;
private ExtendedFloatingActionButton deleteFab;
private FileUtil fileUtil;
private String format = "";
private String website = "";
private String sort = "DESC";
private String searchQuery = "";
private static final String TAG = "downloadsFragment";
public IDownloaderListener listener = new IDownloaderListener() {
public void onDownloadStart(DownloadInfo downloadInfo) {
try{
if(downloadInfo != null){
Video v = downloadInfo.getVideo();
progressBar = fragmentView.findViewWithTag(v.getURL()+v.getDownloadedType()+"##progress");
}
}catch(Exception ignored){}
downloading = true;
}
public void onDownloadProgress(DownloadInfo info) {
activity.runOnUiThread(() -> {
try{
int progress = info.getProgress();
Video v = info.getVideo();
if (progress > 0) {
progressBar = fragmentView.findViewWithTag(v.getURL()+v.getDownloadedType()+"##progress");
progressBar.setProgressCompat(progress, true);
}
}catch(Exception ignored){
}
});
}
public void onDownloadError(DownloadInfo info){
try{
Video item = info.getVideo();
Log.e(TAG, item.toString());
String url = item.getURL();
String type = info.getDownloadType();
Video v = findVideo(url, type);
Log.e(TAG, v.toString());
databaseManager = new DatabaseManager(context);
databaseManager.clearHistoryItem(v, false);
int position = downloadsObjects.indexOf(v);
downloadsRecyclerViewAdapter.notifyItemRemoved(position);
downloadsObjects.remove(v);
if (downloadsObjects.isEmpty()) initCards();
downloading = false;
}catch(Exception e){
e.printStackTrace();
}
}
public void onDownloadEnd(DownloadInfo downloadInfo) {
Video item = downloadInfo.getVideo();
String url = item.getURL();
String type = downloadInfo.getDownloadType();
String path = downloadInfo.getDownloadPath();
item = findVideo(url, type);
int index = downloadsObjects.indexOf(item);
try{
item.setDownloadedType(type);
item.setDownloadPath(path);
Calendar cal = Calendar.getInstance();
Date date = new Date();
cal.setTime(date);
int day = cal.get(Calendar.DAY_OF_MONTH);
String month = cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
int year = cal.get(Calendar.YEAR);
DateFormat formatter = new SimpleDateFormat("HH:mm", Locale.getDefault());
String time = formatter.format(date);
String downloadedTime = day + " " + month + " " + year + " " + time;
String downloadPath = "";
try{
downloadPath = fileUtil.scanMedia(item, context);
}catch(Exception e){
e.printStackTrace();
}
databaseManager = new DatabaseManager(context);
//TODO observe file transfer worker here and then update, but first you need to turn this to kotlin and implement proper mvvm
try {
item.setDownloadedTime(downloadedTime);
item.setDownloadPath(downloadPath);
item.setQueuedDownload(false);
databaseManager.updateHistoryItem(item);
databaseManager.close();
downloadsRecyclerViewAdapter.notifyItemChanged(index);
} catch (Exception ignored) {}
downloading = false;
}catch(Exception ignored){}
}
@Override
public void onDownloadCancel(DownloadInfo info) {
Video v = info.getVideo();
v = findVideo(v.getURL(), v.getDownloadedType());
try {
databaseManager = new DatabaseManager(context);
databaseManager.clearHistoryItem(v,false);
databaseManager.close();
downloadsRecyclerViewAdapter.notifyItemRemoved(downloadsObjects.indexOf(v));
downloadsObjects.remove(v);
if (downloadsObjects.isEmpty()) initCards();
downloading = false;
}catch (Exception ignored){}
}
@Override
public void onDownloadCancelAll(DownloadInfo downloadInfo){
try {
databaseManager = new DatabaseManager(context);
databaseManager.clearDownloadingHistory();
databaseManager.close();
initCards();
downloading = false;
}catch (Exception e){
e.printStackTrace();
}
}
public void onDownloadServiceEnd() {}
};
public void setDownloading(boolean d){
downloading = d;
}
public DownloadsFragment() {
}
@SuppressWarnings("unused")
public static DownloadsFragment newInstance() {
DownloadsFragment fragment = new DownloadsFragment();
Bundle args = new Bundle();
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
fragmentView = inflater.inflate(R.layout.fragment_downloads, container, false);
context = fragmentView.getContext().getApplicationContext();
activity = getActivity();
mainActivity = (MainActivity) activity;
fragmentContext = fragmentView.getContext();
layoutinflater = LayoutInflater.from(context);
shimmerCards = fragmentView.findViewById(R.id.shimmer_downloads_framelayout);
topAppBar = fragmentView.findViewById(R.id.downloads_toolbar);
no_results = fragmentView.findViewById(R.id.downloads_no_results);
selectionChips = fragmentView.findViewById(R.id.downloads_selection_chips);
websiteGroup = fragmentView.findViewById(R.id.website_chip_group);
deleteFab = fragmentView.findViewById(R.id.delete_selected_fab);
fileUtil = new FileUtil();
deleteFab.setTag("deleteSelected");
deleteFab.setOnClickListener(this);
uiHandler = new Handler(Looper.getMainLooper());
downloadsObjects = new ArrayList<>();
selectedObjects = new ArrayList<>();
downloading = mainActivity.isDownloadServiceRunning();
recyclerView = fragmentView.findViewById(R.id.recycler_view_downloads);
downloadsRecyclerViewAdapter = new DownloadsRecyclerViewAdapter(downloadsObjects, this, activity);
recyclerView.setAdapter(downloadsRecyclerViewAdapter);
recyclerView.setNestedScrollingEnabled(false);
initMenu();
initChips();
initCards();
return fragmentView;
}
public void initCards(){
shimmerCards.startShimmer();
shimmerCards.setVisibility(View.VISIBLE);
downloadsRecyclerViewAdapter.clear();
no_results.setVisibility(View.GONE);
selectionChips.setVisibility(View.VISIBLE);
databaseManager = new DatabaseManager(context);
try{
Thread thread = new Thread(() -> {
if (!downloading) databaseManager.clearDownloadingHistory();
downloadsObjects = databaseManager.getHistory("", format,website,sort);
uiHandler.post(() -> {
downloadsRecyclerViewAdapter.add(downloadsObjects);
shimmerCards.stopShimmer();
shimmerCards.setVisibility(View.GONE);
updateWebsiteChips();
});
if(downloadsObjects.size() == 0){
uiHandler.post(() -> {
no_results.setVisibility(View.VISIBLE);
selectionChips.setVisibility(View.GONE);
websiteGroup.removeAllViews();
});
}
databaseManager.close();
});
thread.start();
}catch(Exception e){
Log.e(TAG, e.toString());
}
}
public void scrollToTop(){
recyclerView.scrollToPosition(0);
new Handler(Looper.getMainLooper()).post(() -> ((AppBarLayout) topAppBar.getParent()).setExpanded(true, true));
}
private void initMenu(){
MenuItem.OnActionExpandListener onActionExpandListener = new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem menuItem) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem menuItem) {
return true;
}
};
MaterialToolbar toolbar = fragmentView.findViewById(R.id.downloads_toolbar);
topAppBar.getMenu().findItem(R.id.search_downloads).setOnActionExpandListener(onActionExpandListener);
SearchView searchView = (SearchView) topAppBar.getMenu().findItem(R.id.search_downloads).getActionView();
searchView.setInputType(InputType.TYPE_CLASS_TEXT);
searchView.setQueryHint(getString(R.string.search_history_hint));
databaseManager = new DatabaseManager(context);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchQuery = query;
topAppBar.getMenu().findItem(R.id.search_downloads).collapseActionView();
downloadsObjects = databaseManager.getHistory(query, format,website,sort);
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
if(downloadsObjects.size() == 0) {
no_results.setVisibility(View.VISIBLE);
selectionChips.setVisibility(View.GONE);
websiteGroup.removeAllViews();
}
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
searchQuery = newText;
downloadsObjects = databaseManager.getHistory(newText, format,website,sort);
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
if (downloadsObjects.size() == 0) {
no_results.setVisibility(View.VISIBLE);
selectionChips.setVisibility(View.GONE);
}else{
no_results.setVisibility(View.GONE);
selectionChips.setVisibility(View.VISIBLE);
}
return true;
}
});
topAppBar.setOnClickListener(view -> scrollToTop());
topAppBar.setOnMenuItemClickListener((MenuItem m) -> {
int itemID = m.getItemId();
if(itemID == R.id.remove_downloads) {
if (downloadsObjects.size() == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show();
return true;
}
MaterialAlertDialogBuilder delete_dialog = new MaterialAlertDialogBuilder(fragmentContext);
delete_dialog.setTitle(getString(R.string.confirm_delete_history));
delete_dialog.setMessage(getString(R.string.confirm_delete_history_desc));
delete_dialog.setNegativeButton(getString(R.string.cancel), (dialogInterface, i) -> {
dialogInterface.cancel();
});
delete_dialog.setPositiveButton(getString(R.string.ok), (dialogInterface, i) -> {
databaseManager.clearHistory();
downloadsRecyclerViewAdapter.clear();
downloadsObjects.clear();
no_results.setVisibility(View.VISIBLE);
selectionChips.setVisibility(View.GONE);
websiteGroup.removeAllViews();
});
delete_dialog.show();
}else if(itemID == R.id.remove_deleted_downloads){
if (downloadsObjects.size() == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show();
return true;
}
MaterialAlertDialogBuilder delete_dialog = new MaterialAlertDialogBuilder(fragmentContext);
delete_dialog.setTitle(getString(R.string.confirm_delete_history));
delete_dialog.setMessage(getString(R.string.confirm_delete_history_deleted_desc));
delete_dialog.setNegativeButton(getString(R.string.cancel), (dialogInterface, i) -> {
dialogInterface.cancel();
});
delete_dialog.setPositiveButton(getString(R.string.ok), (dialogInterface, i) -> {
databaseManager.clearDeletedHistory();
initCards();
});
delete_dialog.show();
}else if (itemID == R.id.remove_duplicates){
if (downloadsObjects.size() == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show();
return true;
}
MaterialAlertDialogBuilder delete_dialog = new MaterialAlertDialogBuilder(fragmentContext);
delete_dialog.setTitle(getString(R.string.confirm_delete_history));
delete_dialog.setMessage(getString(R.string.confirm_delete_history_duplicates_desc));
delete_dialog.setNegativeButton(getString(R.string.cancel), (dialogInterface, i) -> {
dialogInterface.cancel();
});
delete_dialog.setPositiveButton(getString(R.string.ok), (dialogInterface, i) -> {
databaseManager.clearDuplicateHistory();
initCards();
});
delete_dialog.show();
}else if (itemID == R.id.remove_downloading){
if (downloadsObjects.size() == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show();
return true;
}
MaterialAlertDialogBuilder delete_dialog = new MaterialAlertDialogBuilder(fragmentContext);
delete_dialog.setTitle(getString(R.string.confirm_delete_history));
delete_dialog.setMessage(getString(R.string.confirm_delete_downloading_desc));
delete_dialog.setNegativeButton(getString(R.string.cancel), (dialogInterface, i) -> {
dialogInterface.cancel();
});
delete_dialog.setPositiveButton(getString(R.string.ok), (dialogInterface, i) -> {
databaseManager.clearDownloadingHistory();
mainActivity.cancelDownloadService();
initCards();
});
delete_dialog.show();
}
return true;
});
}
private void initChips(){
//sort and history/downloading switch
Chip sortChip = fragmentView.findViewById(R.id.sort_chip);
sortChip.setOnClickListener(view -> {
sortSheet = new BottomSheetDialog(fragmentContext);
sortSheet.requestWindowFeature(Window.FEATURE_NO_TITLE);
sortSheet.setContentView(R.layout.downloads_sort_sheet);
TextView newest = sortSheet.findViewById(R.id.newest);
TextView oldest = sortSheet.findViewById(R.id.oldest);
newest.setOnClickListener(view1 -> {
sort = "DESC";
downloadsObjects = databaseManager.getHistory(searchQuery, format,website,sort);
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
sortSheet.cancel();
});
oldest.setOnClickListener(view1 -> {
sort = "ASC";
downloadsObjects = databaseManager.getHistory(searchQuery, format,website,sort);
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
sortSheet.cancel();
});
TextView cancel = sortSheet.findViewById(R.id.cancel);
cancel.setOnClickListener(view1 -> {
sortSheet.cancel();
});
sortSheet.show();
sortSheet.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
});
//format
Chip audio = fragmentView.findViewById(R.id.audio_chip);
audio.setOnClickListener(view -> {
if (audio.isChecked()) {
format = "audio";
if (recyclerView.getVisibility() == View.GONE){
}
downloadsObjects = databaseManager.getHistory(searchQuery,format,website,sort);
audio.setChecked(true);
}else {
format = "";
downloadsObjects = databaseManager.getHistory(searchQuery,format,website,sort);
audio.setChecked(false);
}
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
});
Chip video = fragmentView.findViewById(R.id.video_chip);
video.setOnClickListener(view -> {
if (video.isChecked()) {
format = "video";
downloadsObjects = databaseManager.getHistory(searchQuery,format,website,sort);
video.setChecked(true);
}else {
format = "";
downloadsObjects = databaseManager.getHistory(searchQuery,format,website,sort);
video.setChecked(false);
}
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
});
}
private void updateWebsiteChips(){
websiteGroup.removeAllViews();
ArrayList<String> websites = downloadsRecyclerViewAdapter.getWebsites();
for (int i = 0; i < websites.size(); i++){
String w = websites.get(i);
Chip tmp = (Chip) layoutinflater.inflate(R.layout.filter_chip, websiteGroup, false);
tmp.setText(w);
tmp.setId(i);
tmp.setOnClickListener(view -> {
if (tmp.isChecked()){
website = (String) tmp.getText();
downloadsObjects = databaseManager.getHistory(searchQuery,format,website,sort);
websiteGroup.check(view.getId());
}else{
website = "";
downloadsObjects = databaseManager.getHistory(searchQuery,format,website,sort);
websiteGroup.clearCheck();
}
downloadsRecyclerViewAdapter.clear();
downloadsRecyclerViewAdapter.add(downloadsObjects);
});
websiteGroup.addView(tmp);
}
}
@Override
public void onClick(View v) {
int id = v.getId();
if(id == R.id.bottomsheet_remove_button){
removedownloadsItem((Integer) v.getTag());
}else if(id == R.id.bottom_sheet_link){
openLinkIntent((Integer) v.getTag());
}else if(id == R.id.bottomsheet_open_file_button){
openFileIntent((Integer) v.getTag());
}else if (id == R.id.delete_selected_fab){
removeSelectedItems();
}
}
@Override
public boolean onLongClick(View v) {
int id = v.getId();
if(id == R.id.bottom_sheet_link){
copyLinkToClipBoard((Integer) v.getTag());
return true;
}
return false;
}
private void removeSelectedItems(){
if(bottomSheet != null) bottomSheet.hide();
final boolean[] delete_file = {false};
databaseManager = new DatabaseManager(context);
MaterialAlertDialogBuilder delete_dialog = new MaterialAlertDialogBuilder(fragmentContext);
delete_dialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items));
delete_dialog.setMultiChoiceItems(new String[]{getString(R.string.delete_files_too)}, new boolean[]{false}, (dialogInterface, i, b) -> delete_file[0] = b);
delete_dialog.setNegativeButton(getString(R.string.cancel), (dialogInterface, i) -> {
dialogInterface.cancel();
});
delete_dialog.setPositiveButton(getString(R.string.ok), (dialogInterface, i) -> {
for (int j = 0; j < selectedObjects.size(); j++){
Video v = selectedObjects.get(j);
int position = downloadsObjects.indexOf(v);
downloadsObjects.remove(v);
downloadsRecyclerViewAdapter.remove(position);
databaseManager.clearHistoryItem(v, delete_file[0]);
}
updateWebsiteChips();
databaseManager.close();
selectedObjects = new ArrayList<>();
downloadsRecyclerViewAdapter.clearCheckedVideos();
deleteFab.setVisibility(View.GONE);
if(downloadsObjects.size() == 0){
uiHandler.post(() -> {
no_results.setVisibility(View.VISIBLE);
selectionChips.setVisibility(View.GONE);
websiteGroup.removeAllViews();
});
}
});
delete_dialog.show();
}
private void removedownloadsItem(int position){
if(bottomSheet != null) bottomSheet.hide();
final boolean[] delete_file = {false};
databaseManager = new DatabaseManager(context);
Video v = downloadsObjects.get(position);
MaterialAlertDialogBuilder delete_dialog = new MaterialAlertDialogBuilder(fragmentContext);
delete_dialog.setTitle(getString(R.string.you_are_going_to_delete) + " \""+v.getTitle()+"\"!");
delete_dialog.setMultiChoiceItems(new String[]{getString(R.string.delete_file_too)}, new boolean[]{false}, (dialogInterface, i, b) -> delete_file[0] = b);
delete_dialog.setNegativeButton(getString(R.string.cancel), (dialogInterface, i) -> {
dialogInterface.cancel();
});
delete_dialog.setPositiveButton(getString(R.string.ok), (dialogInterface, i) -> {
downloadsObjects.remove(position);
downloadsRecyclerViewAdapter.remove(position);
updateWebsiteChips();
databaseManager.clearHistoryItem(v, delete_file[0]);
databaseManager.close();
if(downloadsObjects.size() == 0){
uiHandler.post(() -> {
no_results.setVisibility(View.VISIBLE);
selectionChips.setVisibility(View.GONE);
websiteGroup.removeAllViews();
});
}
});
delete_dialog.show();
}
private void copyLinkToClipBoard(int position){
String url = downloadsObjects.get(position).getURL();
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.url), url);
clipboard.setPrimaryClip(clip);
if(bottomSheet != null) bottomSheet.hide();
Toast.makeText(context, getString(R.string.link_copied_to_clipboard), Toast.LENGTH_SHORT).show();
}
private void openLinkIntent(int position){
String url =downloadsObjects.get(position).getURL();
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
if(bottomSheet != null) bottomSheet.hide();
startActivity(i);
}
private void openFileIntent(int position){
String downloadPath =downloadsObjects.get(position).getDownloadPath();
File file = new File(downloadPath);
Uri uri = FileProvider.getUriForFile(fragmentContext, fragmentContext.getPackageName() + ".fileprovider", file);
String mime = mainActivity.getContentResolver().getType(uri);
Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(uri, mime);
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(i);
}
@Override
public void onCardClick(int position, boolean isFilePresent) {
bottomSheet = new BottomSheetDialog(fragmentContext);
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE);
bottomSheet.setContentView(R.layout.downloads_bottom_sheet);
Video video = downloadsObjects.get(position);
TextView title = bottomSheet.findViewById(R.id.bottom_sheet_title);
title.setText(video.getTitle());
TextView author = bottomSheet.findViewById(R.id.bottom_sheet_author);
author.setText(video.getAuthor());
Button link = bottomSheet.findViewById(R.id.bottom_sheet_link);
String url = video.getURL();
link.setText(url);
link.setTag(position);
link.setOnClickListener(this);
link.setOnLongClickListener(this);
Button remove = bottomSheet.findViewById(R.id.bottomsheet_remove_button);
remove.setTag(position);
remove.setOnClickListener(this);
Button openFile = bottomSheet.findViewById(R.id.bottomsheet_open_file_button);
openFile.setTag(position);
openFile.setOnClickListener(this);
if (!isFilePresent) openFile.setVisibility(View.GONE);
bottomSheet.show();
bottomSheet.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
public void onCardSelect(int position, boolean add) {
Video video = downloadsObjects.get(position);
if (add) selectedObjects.add(video); else selectedObjects.remove(video);
if(selectedObjects.size() > 1){
deleteFab.setVisibility(View.VISIBLE);
} else {
deleteFab.setVisibility(View.GONE);
}
}
@Override
public void onButtonClick(int position) {
Video vid = downloadsObjects.get(position);
try {
mainActivity.removeItemFromDownloadQueue(vid, vid.getDownloadedType());
}catch (Exception e){
DownloadInfo info = new DownloadInfo();
info.setVideo(vid);
info.setDownloadType(vid.getDownloadedType());
listener.onDownloadCancel(info);
}
}
public Video findVideo(String url, String type) {
for (int i = 0; i < downloadsObjects.size(); i++) {
Video v = downloadsObjects.get(i);
if ((v.getURL()).equals(url) && v.getDownloadedType().equals(type) && v.isQueuedDownload()) {
return v;
}
}
return null;
}
}

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.page; package com.deniscerri.ytdlnis.ui;
import android.Manifest; import android.Manifest;
import android.content.ComponentName; import android.content.ComponentName;
@ -19,7 +19,6 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import com.deniscerri.ytdlnis.R; import com.deniscerri.ytdlnis.R;
import com.deniscerri.ytdlnis.DownloaderService; import com.deniscerri.ytdlnis.DownloaderService;
import com.deniscerri.ytdlnis.database.Video;
import com.deniscerri.ytdlnis.service.DownloadInfo; import com.deniscerri.ytdlnis.service.DownloadInfo;
import com.deniscerri.ytdlnis.service.IDownloaderListener; import com.deniscerri.ytdlnis.service.IDownloaderListener;
import com.deniscerri.ytdlnis.service.IDownloaderService; import com.deniscerri.ytdlnis.service.IDownloaderService;
@ -27,7 +26,6 @@ import com.deniscerri.ytdlnis.util.NotificationUtil;
import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
public class CustomCommandActivity extends AppCompatActivity { public class CustomCommandActivity extends AppCompatActivity {

View file

@ -0,0 +1,554 @@
package com.deniscerri.ytdlnis.ui
import android.app.Activity
import android.content.*
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.InputType
import android.util.Log
import android.view.*
import android.view.View.OnLongClickListener
import android.widget.*
import androidx.appcompat.widget.SearchView
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.DownloadsRecyclerViewAdapter
import com.deniscerri.ytdlnis.database.DatabaseManager
import com.deniscerri.ytdlnis.database.Video
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.service.DownloadInfo
import com.deniscerri.ytdlnis.util.FileUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import java.io.File
/**
* A fragment representing a list of Items.
*/
class DownloadsFragment : Fragment(), DownloadsRecyclerViewAdapter.OnItemClickListener,
View.OnClickListener, OnLongClickListener {
private lateinit var historyViewModel : HistoryViewModel
private var downloading = false
private var fragmentView: View? = null
private var databaseManager: DatabaseManager? = null
var activity: Activity? = null
var mainActivity: MainActivity? = null
var fragmentContext: Context? = null
private var layoutinflater: LayoutInflater? = null
private var shimmerCards: ShimmerFrameLayout? = null
private var topAppBar: MaterialToolbar? = null
private var recyclerView: RecyclerView? = null
private var downloadsRecyclerViewAdapter: DownloadsRecyclerViewAdapter? = null
private var bottomSheet: BottomSheetDialog? = null
private var sortSheet: BottomSheetDialog? = null
private var uiHandler: Handler? = null
private var no_results: RelativeLayout? = null
private var selectionChips: LinearLayout? = null
private var websiteGroup: ChipGroup? = null
private var downloadsObjects: ArrayList<Video?>? = null
var selectedObjects: ArrayList<Video?>? = null
private var progressBar: LinearProgressIndicator? = null
private var deleteFab: ExtendedFloatingActionButton? = null
private var fileUtil: FileUtil? = null
private var format = ""
private var website = ""
private var sort = "DESC"
private var searchQuery = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_downloads, container, false)
activity = getActivity()
mainActivity = activity as MainActivity?
fragmentContext = context?.applicationContext
layoutinflater = LayoutInflater.from(context)
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
shimmerCards = this.view?.findViewById(R.id.shimmer_downloads_framelayout)
topAppBar = this.view?.findViewById(R.id.downloads_toolbar)
no_results = this.view?.findViewById(R.id.downloads_no_results)
selectionChips = this.view?.findViewById(R.id.downloads_selection_chips)
websiteGroup = this.view?.findViewById(R.id.website_chip_group)
deleteFab = this.view?.findViewById(R.id.delete_selected_fab)
fileUtil = FileUtil()
deleteFab?.tag = "deleteSelected"
deleteFab?.setOnClickListener(this)
uiHandler = Handler(Looper.getMainLooper())
downloadsObjects = ArrayList()
selectedObjects = ArrayList()
downloading = mainActivity!!.isDownloadServiceRunning()
recyclerView = this.view?.findViewById(R.id.recycler_view_downloads)
downloadsRecyclerViewAdapter =
DownloadsRecyclerViewAdapter(downloadsObjects, this, activity)
recyclerView?.adapter = downloadsRecyclerViewAdapter
recyclerView?.isNestedScrollingEnabled = false
initMenu()
initChips()
initCards()
return fragmentView
}
fun initCards() {
shimmerCards!!.startShimmer()
shimmerCards!!.visibility = View.VISIBLE
downloadsRecyclerViewAdapter!!.clear()
no_results!!.visibility = View.GONE
selectionChips!!.visibility = View.VISIBLE
databaseManager = DatabaseManager(context)
try {
val thread = Thread {
if (!downloading) databaseManager!!.clearDownloadingHistory()
downloadsObjects = databaseManager!!.getHistory("", format, website, sort)
uiHandler!!.post {
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
shimmerCards!!.stopShimmer()
shimmerCards!!.visibility = View.GONE
updateWebsiteChips()
}
if ((downloadsObjects as ArrayList<Video>?)?.size == 0) {
uiHandler!!.post {
no_results!!.visibility = View.VISIBLE
selectionChips!!.visibility = View.GONE
websiteGroup!!.removeAllViews()
}
}
databaseManager!!.close()
}
thread.start()
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
}
fun scrollToTop() {
recyclerView!!.scrollToPosition(0)
Handler(Looper.getMainLooper()).post {
(topAppBar!!.parent as AppBarLayout).setExpanded(
true,
true
)
}
}
private fun initMenu() {
val onActionExpandListener: MenuItem.OnActionExpandListener =
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
return true
}
}
val toolbar = fragmentView!!.findViewById<MaterialToolbar>(R.id.downloads_toolbar)
topAppBar!!.menu.findItem(R.id.search_downloads)
.setOnActionExpandListener(onActionExpandListener)
val searchView = topAppBar!!.menu.findItem(R.id.search_downloads).actionView as SearchView?
searchView!!.inputType = InputType.TYPE_CLASS_TEXT
searchView.queryHint = getString(R.string.search_history_hint)
databaseManager = DatabaseManager(context)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
searchQuery = query
topAppBar!!.menu.findItem(R.id.search_downloads).collapseActionView()
downloadsObjects = databaseManager!!.getHistory(query, format, website, sort)
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
if ((downloadsObjects as ArrayList<Video>?)?.size == 0) {
no_results!!.visibility = View.VISIBLE
selectionChips!!.visibility = View.GONE
websiteGroup!!.removeAllViews()
}
return true
}
override fun onQueryTextChange(newText: String): Boolean {
searchQuery = newText
downloadsObjects = databaseManager!!.getHistory(newText, format, website, sort)
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
if ((downloadsObjects as ArrayList<Video>?)?.size == 0) {
no_results!!.visibility = View.VISIBLE
selectionChips!!.visibility = View.GONE
} else {
no_results!!.visibility = View.GONE
selectionChips!!.visibility = View.VISIBLE
}
return true
}
})
topAppBar!!.setOnClickListener { view: View? -> scrollToTop() }
topAppBar!!.setOnMenuItemClickListener { m: MenuItem ->
val itemID = m.itemId
if (itemID == R.id.remove_downloads) {
if (downloadsObjects!!.size == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
return@setOnMenuItemClickListener true
}
val delete_dialog = MaterialAlertDialogBuilder(fragmentContext!!)
delete_dialog.setTitle(getString(R.string.confirm_delete_history))
delete_dialog.setMessage(getString(R.string.confirm_delete_history_desc))
delete_dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, i: Int -> dialogInterface.cancel() }
delete_dialog.setPositiveButton(getString(R.string.ok)) { dialogInterface: DialogInterface?, i: Int ->
databaseManager!!.clearHistory()
downloadsRecyclerViewAdapter!!.clear()
downloadsObjects!!.clear()
no_results!!.visibility = View.VISIBLE
selectionChips!!.visibility = View.GONE
websiteGroup!!.removeAllViews()
}
delete_dialog.show()
} else if (itemID == R.id.remove_deleted_downloads) {
if (downloadsObjects!!.size == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
return@setOnMenuItemClickListener true
}
val delete_dialog = MaterialAlertDialogBuilder(fragmentContext!!)
delete_dialog.setTitle(getString(R.string.confirm_delete_history))
delete_dialog.setMessage(getString(R.string.confirm_delete_history_deleted_desc))
delete_dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, i: Int -> dialogInterface.cancel() }
delete_dialog.setPositiveButton(getString(R.string.ok)) { dialogInterface: DialogInterface?, i: Int ->
databaseManager!!.clearDeletedHistory()
initCards()
}
delete_dialog.show()
} else if (itemID == R.id.remove_duplicates) {
if (downloadsObjects!!.size == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
return@setOnMenuItemClickListener true
}
val delete_dialog = MaterialAlertDialogBuilder(fragmentContext!!)
delete_dialog.setTitle(getString(R.string.confirm_delete_history))
delete_dialog.setMessage(getString(R.string.confirm_delete_history_duplicates_desc))
delete_dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, i: Int -> dialogInterface.cancel() }
delete_dialog.setPositiveButton(getString(R.string.ok)) { dialogInterface: DialogInterface?, i: Int ->
databaseManager!!.clearDuplicateHistory()
initCards()
}
delete_dialog.show()
} else if (itemID == R.id.remove_downloading) {
if (downloadsObjects!!.size == 0) {
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
return@setOnMenuItemClickListener true
}
val delete_dialog = MaterialAlertDialogBuilder(fragmentContext!!)
delete_dialog.setTitle(getString(R.string.confirm_delete_history))
delete_dialog.setMessage(getString(R.string.confirm_delete_downloading_desc))
delete_dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, i: Int -> dialogInterface.cancel() }
delete_dialog.setPositiveButton(getString(R.string.ok)) { dialogInterface: DialogInterface?, i: Int ->
databaseManager!!.clearDownloadingHistory()
mainActivity!!.cancelDownloadService()
initCards()
}
delete_dialog.show()
}
true
}
}
private fun initChips() {
//sort and history/downloading switch
val sortChip = fragmentView!!.findViewById<Chip>(R.id.sort_chip)
sortChip.setOnClickListener { view: View? ->
sortSheet = BottomSheetDialog(fragmentContext!!)
sortSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE)
sortSheet!!.setContentView(R.layout.downloads_sort_sheet)
val newest = sortSheet!!.findViewById<TextView>(R.id.newest)
val oldest = sortSheet!!.findViewById<TextView>(R.id.oldest)
newest!!.setOnClickListener { view1: View? ->
sort = "DESC"
downloadsObjects = databaseManager!!.getHistory(searchQuery, format, website, sort)
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
sortSheet!!.cancel()
}
oldest!!.setOnClickListener { view1: View? ->
sort = "ASC"
downloadsObjects = databaseManager!!.getHistory(searchQuery, format, website, sort)
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
sortSheet!!.cancel()
}
val cancel = sortSheet!!.findViewById<TextView>(R.id.cancel)
cancel!!.setOnClickListener { view1: View? -> sortSheet!!.cancel() }
sortSheet!!.show()
sortSheet!!.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
//format
val audio = fragmentView!!.findViewById<Chip>(R.id.audio_chip)
audio.setOnClickListener { view: View? ->
if (audio.isChecked) {
format = "audio"
if (recyclerView!!.visibility == View.GONE) {
}
downloadsObjects = databaseManager!!.getHistory(searchQuery, format, website, sort)
audio.isChecked = true
} else {
format = ""
downloadsObjects = databaseManager!!.getHistory(searchQuery, format, website, sort)
audio.isChecked = false
}
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
}
val video = fragmentView!!.findViewById<Chip>(R.id.video_chip)
video.setOnClickListener { view: View? ->
if (video.isChecked) {
format = "video"
downloadsObjects = databaseManager!!.getHistory(searchQuery, format, website, sort)
video.isChecked = true
} else {
format = ""
downloadsObjects = databaseManager!!.getHistory(searchQuery, format, website, sort)
video.isChecked = false
}
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
}
}
private fun updateWebsiteChips() {
websiteGroup!!.removeAllViews()
val websites = downloadsRecyclerViewAdapter!!.websites
for (i in websites.indices) {
val w = websites[i]
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip
tmp.text = w
tmp.id = i
tmp.setOnClickListener { view: View ->
if (tmp.isChecked) {
website = tmp.text as String
downloadsObjects =
databaseManager!!.getHistory(searchQuery, format, website, sort)
websiteGroup!!.check(view.id)
} else {
website = ""
downloadsObjects =
databaseManager!!.getHistory(searchQuery, format, website, sort)
websiteGroup!!.clearCheck()
}
downloadsRecyclerViewAdapter!!.clear()
downloadsRecyclerViewAdapter!!.add(downloadsObjects)
}
websiteGroup!!.addView(tmp)
}
}
override fun onClick(v: View) {
val id = v.id
if (id == R.id.bottomsheet_remove_button) {
removedownloadsItem(v.tag as Int)
} else if (id == R.id.bottom_sheet_link) {
openLinkIntent(v.tag as Int)
} else if (id == R.id.bottomsheet_open_file_button) {
openFileIntent(v.tag as Int)
} else if (id == R.id.delete_selected_fab) {
removeSelectedItems()
}
}
override fun onLongClick(v: View): Boolean {
val id = v.id
if (id == R.id.bottom_sheet_link) {
copyLinkToClipBoard(v.tag as Int)
return true
}
return false
}
private fun removeSelectedItems() {
if (bottomSheet != null) bottomSheet!!.hide()
val delete_file = booleanArrayOf(false)
databaseManager = DatabaseManager(context)
val delete_dialog = MaterialAlertDialogBuilder(fragmentContext!!)
delete_dialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
delete_dialog.setMultiChoiceItems(
arrayOf(getString(R.string.delete_files_too)),
booleanArrayOf(false)
) { dialogInterface: DialogInterface?, i: Int, b: Boolean -> delete_file[0] = b }
delete_dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, i: Int -> dialogInterface.cancel() }
delete_dialog.setPositiveButton(getString(R.string.ok)) { dialogInterface: DialogInterface?, i: Int ->
for (j in selectedObjects!!.indices) {
val v = selectedObjects!![j]
val position = downloadsObjects!!.indexOf(v)
downloadsObjects!!.remove(v)
downloadsRecyclerViewAdapter!!.remove(position)
databaseManager!!.clearHistoryItem(v, delete_file[0])
}
updateWebsiteChips()
databaseManager!!.close()
selectedObjects = ArrayList()
downloadsRecyclerViewAdapter!!.clearCheckedVideos()
deleteFab!!.visibility = View.GONE
if (downloadsObjects!!.size == 0) {
uiHandler!!.post {
no_results!!.visibility = View.VISIBLE
selectionChips!!.visibility = View.GONE
websiteGroup!!.removeAllViews()
}
}
}
delete_dialog.show()
}
private fun removedownloadsItem(position: Int) {
if (bottomSheet != null) bottomSheet!!.hide()
val delete_file = booleanArrayOf(false)
databaseManager = DatabaseManager(context)
val v = downloadsObjects!![position]
val delete_dialog = MaterialAlertDialogBuilder(fragmentContext!!)
delete_dialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + v!!.title + "\"!")
delete_dialog.setMultiChoiceItems(
arrayOf(getString(R.string.delete_file_too)),
booleanArrayOf(false)
) { dialogInterface: DialogInterface?, i: Int, b: Boolean -> delete_file[0] = b }
delete_dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, i: Int -> dialogInterface.cancel() }
delete_dialog.setPositiveButton(getString(R.string.ok)) { dialogInterface: DialogInterface?, i: Int ->
downloadsObjects!!.removeAt(position)
downloadsRecyclerViewAdapter!!.remove(position)
updateWebsiteChips()
databaseManager!!.clearHistoryItem(v, delete_file[0])
databaseManager!!.close()
if (downloadsObjects!!.size == 0) {
uiHandler!!.post {
no_results!!.visibility = View.VISIBLE
selectionChips!!.visibility = View.GONE
websiteGroup!!.removeAllViews()
}
}
}
delete_dialog.show()
}
private fun copyLinkToClipBoard(position: Int) {
val url = downloadsObjects!![position]!!.getURL()
val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(getString(R.string.url), url)
clipboard.setPrimaryClip(clip)
if (bottomSheet != null) bottomSheet!!.hide()
Toast.makeText(context, getString(R.string.link_copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
private fun openLinkIntent(position: Int) {
val url = downloadsObjects!![position]!!.getURL()
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(url)
if (bottomSheet != null) bottomSheet!!.hide()
startActivity(i)
}
private fun openFileIntent(position: Int) {
val downloadPath = downloadsObjects!![position]!!.downloadPath
val file = File(downloadPath)
val uri = FileProvider.getUriForFile(
fragmentContext!!,
fragmentContext!!.packageName + ".fileprovider",
file
)
val mime = mainActivity!!.contentResolver.getType(uri)
val i = Intent(Intent.ACTION_VIEW)
i.setDataAndType(uri, mime)
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(i)
}
override fun onCardClick(position: Int, isFilePresent: Boolean) {
bottomSheet = BottomSheetDialog(fragmentContext!!)
bottomSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet!!.setContentView(R.layout.downloads_bottom_sheet)
val video = downloadsObjects!![position]
val title = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_title)
title!!.text = video!!.title
val author = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = video.author
val link = bottomSheet!!.findViewById<Button>(R.id.bottom_sheet_link)
val url = video.getURL()
link!!.text = url
link.tag = position
link.setOnClickListener(this)
link.setOnLongClickListener(this)
val remove = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = position
remove.setOnClickListener(this)
val openFile = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.tag = position
openFile.setOnClickListener(this)
if (!isFilePresent) openFile.visibility = View.GONE
bottomSheet!!.show()
bottomSheet!!.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun onCardSelect(position: Int, add: Boolean) {
val video = downloadsObjects!![position]
if (add) selectedObjects!!.add(video) else selectedObjects!!.remove(video)
if (selectedObjects!!.size > 1) {
deleteFab!!.visibility = View.VISIBLE
} else {
deleteFab!!.visibility = View.GONE
}
}
override fun onButtonClick(position: Int) {
val vid = downloadsObjects!![position]
try {
mainActivity!!.removeItemFromDownloadQueue(vid, vid!!.downloadedType)
} catch (e: Exception) {
val info = DownloadInfo()
info.video = vid
info.downloadType = vid!!.downloadedType
}
}
fun findVideo(url: String, type: String): Video? {
for (i in downloadsObjects!!.indices) {
val v = downloadsObjects!![i]
if (v!!.getURL() == url && v.downloadedType == type && v.isQueuedDownload) {
return v
}
}
return null
}
companion object {
private const val TAG = "downloadsFragment"
fun newInstance(): DownloadsFragment {
val fragment = DownloadsFragment()
val args = Bundle()
return fragment
}
}
}

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.page; package com.deniscerri.ytdlnis.ui;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.page; package com.deniscerri.ytdlnis.ui;
import android.os.Bundle; import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.page.settings; package com.deniscerri.ytdlnis.ui.settings;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdlnis.page.settings package com.deniscerri.ytdlnis.ui.settings
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent

View file

@ -0,0 +1,36 @@
package com.deniscerri.ytdlnis.work;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.work.ForegroundInfo;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.deniscerri.ytdlnis.MainActivity;
import com.deniscerri.ytdlnis.database.Video;
import java.util.ArrayList;
import kotlin.random.Random;
public class DownloadWorker extends Worker {
private Context context;
private WorkerParameters workerParams;
public DownloadWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
this.context = appContext;
this.workerParams = workerParams;
}
@NonNull
@Override
public Result doWork() {
//TODO
return Result.success();
}
}

View file

@ -10,7 +10,6 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.page.CustomCommandActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import java.io.File import java.io.File

View file

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.page.CustomCommandActivity"> tools:context="com.deniscerri.ytdlnis.ui.CustomCommandActivity">
<FrameLayout <FrameLayout
android:id="@+id/custom_command_frame_layout" android:id="@+id/custom_command_frame_layout"

View file

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.page.settings.SettingsActivity"> tools:context="com.deniscerri.ytdlnis.ui.settings.SettingsActivity">
<FrameLayout <FrameLayout
android:id="@+id/settings_frame_layout" android:id="@+id/settings_frame_layout"

View file

@ -26,6 +26,8 @@ buildscript {
composeVer = "1.3.2" composeVer = "1.3.2"
kotlinVer = "1.7.20" kotlinVer = "1.7.20"
coroutineVer = "1.6.4" coroutineVer = "1.6.4"
retrofitVer = "2.9.0"
kodeinVer = "7.16.0"
} }
repositories { repositories {
mavenCentral() mavenCentral()

View file

@ -13,6 +13,7 @@ dependencyResolutionManagement {
jcenter() jcenter()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://dl.bintray.com/kodein-framework/Kodein-DI" }
} }
} }
rootProject.name = "YTDLnis" rootProject.name = "YTDLnis"