Merge branch 'libre-tube:master' into master

This commit is contained in:
XelXen 2022-11-24 09:57:40 +05:30 committed by GitHub
commit e59c251437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
259 changed files with 8608 additions and 3234 deletions

67
.github/tg.py vendored
View File

@ -1,33 +1,34 @@
import telegram
from tgconfig import *
from json import load
import multiprocessing
from os import system
from time import sleep as wait
def deploy():
system(f'./exec --local --api-id={TG_API_ID} --api-hash={TG_API_HASH}')
def bot():
wait(10)
f = open('commit.json')
data = load(f)
f.close()
bot = telegram.Bot(TG_TOKEN, base_url="http://0.0.0.0:8081/bot")
bot.send_photo(TG_POST_ID, open('alpha.png', 'rb'), f'''*Libretube {data['sha'][0:7]} // Alpha*
[{data['commit']['message']}]({data['html_url']})
Signed-off-by: {data['commit']['author']['name']}
''', parse_mode=telegram.ParseMode.MARKDOWN)
bot.send_media_group(TG_POST_ID, [telegram.InputMediaDocument(open('app-universal-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-x86-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-x86_64-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-armeabi-v7a-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-arm64-v8a-debug.apk', 'rb'))])
system('killall -9 python')
if __name__ == '__main__':
multideploy = multiprocessing.Process(target=deploy)
multibot = multiprocessing.Process(target=bot)
multideploy.start()
multibot.start()
multideploy.join()
multibot.join()
import asyncio
from json import load
from os import listdir
from pyrogram import Client
from pyrogram.types import InputMediaDocument
from tgconfig import *
files = listdir()
mediadocuments = [
InputMediaDocument(file) for file in files if file.endswith("signed.apk")
]
with open("commit.json") as f:
data = load(f)
caption = f"""**Libretube {data['sha'][0:7]} // Alpha**
<a href="{data['html_url']}">{data['commit']['message']}</a>
Signed-off-by: {data['commit']['author']['name']}
"""
async def main():
async with Client("libretube", TG_API_ID, TG_API_HASH, bot_token=TG_TOKEN) as app:
await app.send_photo(
int(TG_POST_ID), "https://libre-tube.github.io/images/Alpha.png", caption
)
await app.send_media_group(int(TG_POST_ID), mediadocuments)
asyncio.run(main())

View File

@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-python@v3
- uses: actions/setup-python@v4
with:
python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@ -54,6 +54,18 @@ jobs:
cd ..
./gradlew assembleDebug
- name: Sign Apk
continue-on-error: true
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: app/build/outputs/apk/debug
signingKey: ${{ secrets.ANDROID_SIGNING_KEY }}
keyAlias: ${{ secrets.ANDROID_KEY_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: Upload to Archive
continue-on-error: true
run: |
@ -61,7 +73,7 @@ jobs:
echo "GH_REPO = '${{ github.repository }}'" > tgconfig.py
git clone https://github.com/LibreTubeAlpha/Archive archive
rm -rf archive/*.apk
mv app/build/outputs/apk/debug/*.apk archive/
mv app/build/outputs/apk/debug/*-signed.apk archive/
cd archive
python ../uploader.py
@ -69,15 +81,12 @@ jobs:
continue-on-error: true
run: |
cd archive
curl https://libre-tube.github.io/images/Alpha.png --output alpha.png
chmod 755 ./exec
mv ../tgconfig.py .
echo "TG_TOKEN = '${{ secrets.TG_TOKEN }}'" >> tgconfig.py
echo "TG_API_ID = '${{ secrets.TG_API_ID }}'" >> tgconfig.py
echo "TG_POST_ID = '${{ secrets.TG_POST_ID }}'" >> tgconfig.py
echo "TG_API_HASH = '${{ secrets.TG_API_HASH }}'" >> tgconfig.py
python -m pip install --upgrade pip
pip install python-telegram-bot
python -m pip install --upgrade pip TgCrypto Pyrogram
mv ../.github/tg.py .
mv ../.github/commit.json .
python tg.py
@ -86,4 +95,5 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: app
path: archive/*.apk
path: archive/*-signed.apk

View File

@ -85,7 +85,7 @@ If creating a pull request, please make sure to format your code (preferred ktli
<img src="https://hosted.weblate.org/widgets/libretube/-/287x66-grey.png" alt="Translation status" />
</a>
## 🌗 Differences from NewPipe
## 🌗 Differences to NewPipe
With NewPipe, the extraction is done locally on your phone, and all the requests sent towards YouTube/Google are done directly from the network you're connected to, which doesn't use a middleman server in between. Therefore, Google can still access information such as the user's IP address. Aside from that, subscriptions can only be stored locally.

View File

@ -6,8 +6,7 @@ This represents the larger, bigger impact features and enhancements we have plan
Feel free to help us if you have any knowledge concerning the following planned features or anything else you imagine.
## Planned
- Support for local playlists
- Various smaller features
- Currently only various smaller features
## Not planned
- Google/MicroG Login

View File

@ -13,11 +13,17 @@ android {
applicationId 'com.github.libretube'
minSdk 21
targetSdk 33
versionCode 20
versionName '0.6.1'
versionCode 23
versionName '0.8.0'
multiDexEnabled true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
resValue "string", "app_name", "LibreTube"
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildFeatures {
@ -41,6 +47,8 @@ android {
}
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable true
applicationIdSuffix ".debug"
resValue "string", "app_name", "LibreTube Debug"
@ -101,6 +109,7 @@ dependencies {
implementation libs.exoplayer
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
implementation libs.exoplayer.extension.mediasession
implementation libs.exoplayer.dash
/* Retrofit and Jackson */
implementation libs.square.retrofit

View File

@ -16,15 +16,16 @@
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable
# prevents obfuscation in debug logs
-dontobfuscate
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#uncomment for debug
#-keepnames class **
# Keep data classes used for Retrofit
-keep class com.github.libretube.obj.** { *; }
# prevents android from removing it
-keep class com.github.libretube.obj.**.** { *; }
# prevents obfuscation in debug logs
-dontobfuscate
-keep class com.github.libretube.obj.update.** { *; }

View File

@ -11,36 +11,10 @@
"type": "UNIVERSAL",
"filters": [],
"attributes": [],
"versionCode": 20,
"versionName": "0.6.1",
"versionCode": 23,
"versionName": "0.8.0",
"outputFile": "app-universal-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 20,
"versionName": "0.6.1",
"outputFile": "app-arm64-v8a-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "armeabi-v7a"
}
],
"attributes": [],
"versionCode": 20,
"versionName": "0.6.1",
"outputFile": "app-armeabi-v7a-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
@ -50,10 +24,23 @@
}
],
"attributes": [],
"versionCode": 20,
"versionName": "0.6.1",
"versionCode": 23,
"versionName": "0.8.0",
"outputFile": "app-x86-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 23,
"versionName": "0.8.0",
"outputFile": "app-arm64-v8a-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
@ -63,9 +50,22 @@
}
],
"attributes": [],
"versionCode": 20,
"versionName": "0.6.1",
"versionCode": 23,
"versionName": "0.8.0",
"outputFile": "app-x86_64-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "armeabi-v7a"
}
],
"attributes": [],
"versionCode": 23,
"versionName": "0.8.0",
"outputFile": "app-armeabi-v7a-release.apk"
}
],
"elementType": "File"

View File

@ -0,0 +1,174 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "c9803a67ce206dbda6e44ed761f80136",
"entities": [
{
"tableName": "watchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "watchPosition",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
"fields": [
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"query"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "customInstance",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiUrl",
"columnName": "apiUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "frontendUrl",
"columnName": "frontendUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "localSubscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
"fields": [
{
"fieldPath": "channelId",
"columnName": "channelId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"channelId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c9803a67ce206dbda6e44ed761f80136')"
]
}
}

View File

@ -0,0 +1,224 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "eb8d0ff1131448df6216b549bbfa7c21",
"entities": [
{
"tableName": "watchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "watchPosition",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
"fields": [
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"query"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "customInstance",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiUrl",
"columnName": "apiUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "frontendUrl",
"columnName": "frontendUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "localSubscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
"fields": [
{
"fieldPath": "channelId",
"columnName": "channelId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"channelId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "playlistBookmark",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, PRIMARY KEY(`playlistId`))",
"fields": [
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistName",
"columnName": "playlistName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"playlistId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb8d0ff1131448df6216b549bbfa7c21')"
]
}
}

View File

@ -0,0 +1,330 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "8c1e428cb526415347639e49f7757f76",
"entities": [
{
"tableName": "watchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "watchPosition",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
"fields": [
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"query"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "customInstance",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiUrl",
"columnName": "apiUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "frontendUrl",
"columnName": "frontendUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "localSubscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
"fields": [
{
"fieldPath": "channelId",
"columnName": "channelId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"channelId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "playlistBookmark",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, PRIMARY KEY(`playlistId`))",
"fields": [
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistName",
"columnName": "playlistName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"playlistId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LocalPlaylist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LocalPlaylistItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8c1e428cb526415347639e49f7757f76')"
]
}
}

View File

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -20,7 +27,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/StartupTheme"
tools:targetApi="n">
tools:targetApi="n"
android:banner="@mipmap/ic_launcher">
<activity
android:name=".ui.activities.NoInternetActivity"
@ -59,6 +67,9 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity-alias
@ -79,6 +90,32 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".DefaultLight"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:enabled="false"
android:exported="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher_light"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_light_round"
android:supportsPictureInPicture="true"
android:targetActivity=".ui.activities.MainActivity"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
@ -99,6 +136,9 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
@ -119,6 +159,9 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
@ -137,6 +180,9 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
@ -157,6 +203,9 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
@ -177,6 +226,9 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
@ -197,6 +249,9 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity
@ -298,6 +353,7 @@
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="piped.video" />
<data android:host="piped.tokhmi.xyz" />
<data android:host="piped.kavin.rocks" />
<data android:host="piped.silkky.cloud" />
@ -329,4 +385,4 @@
android:exported="false" />
</application>
</manifest>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -16,6 +16,7 @@ import com.github.libretube.util.ExceptionHandler
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NotificationHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ProxyHelper
class LibreTubeApp : Application() {
override fun onCreate() {
@ -52,10 +53,16 @@ class LibreTubeApp : Application() {
/**
* Initialize the notification listener in the background
*/
NotificationHelper(this).enqueueWork(
NotificationHelper.enqueueWork(
context = this,
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP
)
/**
* Fetch the image proxy URL for local playlists and the watch history
*/
ProxyHelper.fetchProxyUrl()
/**
* Handler for uncaught exceptions
*/

View File

@ -1,5 +1,22 @@
package com.github.libretube.api
import com.github.libretube.api.obj.Channel
import com.github.libretube.api.obj.ChannelTabResponse
import com.github.libretube.api.obj.CommentsPage
import com.github.libretube.api.obj.DeleteUserRequest
import com.github.libretube.api.obj.Login
import com.github.libretube.api.obj.Message
import com.github.libretube.api.obj.PipedConfig
import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.SearchResult
import com.github.libretube.api.obj.SegmentData
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams
import com.github.libretube.api.obj.Subscribe
import com.github.libretube.api.obj.Subscription
import com.github.libretube.api.obj.Token
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@ -8,81 +25,90 @@ import retrofit2.http.Path
import retrofit2.http.Query
interface PipedApi {
@GET("config")
suspend fun getConfig(): PipedConfig
@GET("trending")
suspend fun getTrending(@Query("region") region: String): List<com.github.libretube.api.obj.StreamItem>
suspend fun getTrending(@Query("region") region: String): List<StreamItem>
@GET("streams/{videoId}")
suspend fun getStreams(@Path("videoId") videoId: String): com.github.libretube.api.obj.Streams
suspend fun getStreams(@Path("videoId") videoId: String): Streams
@GET("comments/{videoId}")
suspend fun getComments(@Path("videoId") videoId: String): com.github.libretube.api.obj.CommentsPage
suspend fun getComments(@Path("videoId") videoId: String): CommentsPage
@GET("sponsors/{videoId}")
suspend fun getSegments(
@Path("videoId") videoId: String,
@Query("category") category: String
): com.github.libretube.api.obj.Segments
): SegmentData
@GET("nextpage/comments/{videoId}")
suspend fun getCommentsNextPage(
@Path("videoId") videoId: String,
@Query("nextpage") nextPage: String
): com.github.libretube.api.obj.CommentsPage
): CommentsPage
@GET("search")
suspend fun getSearchResults(
@Query("q") searchQuery: String,
@Query("filter") filter: String
): com.github.libretube.api.obj.SearchResult
): SearchResult
@GET("nextpage/search")
suspend fun getSearchResultsNextPage(
@Query("q") searchQuery: String,
@Query("filter") filter: String,
@Query("nextpage") nextPage: String
): com.github.libretube.api.obj.SearchResult
): SearchResult
@GET("suggestions")
suspend fun getSuggestions(@Query("query") query: String): List<String>
@GET("channel/{channelId}")
suspend fun getChannel(@Path("channelId") channelId: String): com.github.libretube.api.obj.Channel
suspend fun getChannel(@Path("channelId") channelId: String): Channel
@GET("channels/tabs")
suspend fun getChannelTab(
@Query("data") data: String,
@Query("nextpage") nextPage: String? = null
): ChannelTabResponse
@GET("user/{name}")
suspend fun getChannelByName(@Path("name") channelName: String): com.github.libretube.api.obj.Channel
suspend fun getChannelByName(@Path("name") channelName: String): Channel
@GET("nextpage/channel/{channelId}")
suspend fun getChannelNextPage(
@Path("channelId") channelId: String,
@Query("nextpage") nextPage: String
): com.github.libretube.api.obj.Channel
): Channel
@GET("playlists/{playlistId}")
suspend fun getPlaylist(@Path("playlistId") playlistId: String): com.github.libretube.api.obj.Playlist
suspend fun getPlaylist(@Path("playlistId") playlistId: String): Playlist
@GET("nextpage/playlists/{playlistId}")
suspend fun getPlaylistNextPage(
@Path("playlistId") playlistId: String,
@Query("nextpage") nextPage: String
): com.github.libretube.api.obj.Playlist
): Playlist
@POST("login")
suspend fun login(@Body login: com.github.libretube.api.obj.Login): com.github.libretube.api.obj.Token
suspend fun login(@Body login: Login): Token
@POST("register")
suspend fun register(@Body login: com.github.libretube.api.obj.Login): com.github.libretube.api.obj.Token
suspend fun register(@Body login: Login): Token
@POST("user/delete")
suspend fun deleteAccount(
@Header("Authorization") token: String,
@Body password: com.github.libretube.api.obj.DeleteUserRequest
@Body password: DeleteUserRequest
)
@GET("feed")
suspend fun getFeed(@Query("authToken") token: String?): List<com.github.libretube.api.obj.StreamItem>
suspend fun getFeed(@Query("authToken") token: String?): List<StreamItem>
@GET("feed/unauthenticated")
suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List<com.github.libretube.api.obj.StreamItem>
suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List<StreamItem>
@GET("subscribed")
suspend fun isSubscribed(
@ -91,66 +117,66 @@ interface PipedApi {
): com.github.libretube.api.obj.Subscribed
@GET("subscriptions")
suspend fun subscriptions(@Header("Authorization") token: String): List<com.github.libretube.api.obj.Subscription>
suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription>
@GET("subscriptions/unauthenticated")
suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List<com.github.libretube.api.obj.Subscription>
suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List<Subscription>
@POST("subscribe")
suspend fun subscribe(
@Header("Authorization") token: String,
@Body subscribe: com.github.libretube.api.obj.Subscribe
): com.github.libretube.api.obj.Message
@Body subscribe: Subscribe
): Message
@POST("unsubscribe")
suspend fun unsubscribe(
@Header("Authorization") token: String,
@Body subscribe: com.github.libretube.api.obj.Subscribe
): com.github.libretube.api.obj.Message
@Body subscribe: Subscribe
): Message
@POST("import")
suspend fun importSubscriptions(
@Query("override") override: Boolean,
@Header("Authorization") token: String,
@Body channels: List<String>
): com.github.libretube.api.obj.Message
): Message
@POST("import/playlist")
suspend fun importPlaylist(
@Header("Authorization") token: String,
@Body playlistId: com.github.libretube.api.obj.PlaylistId
): com.github.libretube.api.obj.Message
@Body playlistId: PlaylistId
): PlaylistId
@GET("user/playlists")
suspend fun playlists(@Header("Authorization") token: String): List<com.github.libretube.api.obj.Playlists>
suspend fun getUserPlaylists(@Header("Authorization") token: String): List<Playlists>
@POST("user/playlists/rename")
suspend fun renamePlaylist(
@Header("Authorization") token: String,
@Body playlistId: com.github.libretube.api.obj.PlaylistId
@Body playlistId: PlaylistId
)
@POST("user/playlists/delete")
suspend fun deletePlaylist(
@Header("Authorization") token: String,
@Body playlistId: com.github.libretube.api.obj.PlaylistId
): com.github.libretube.api.obj.Message
@Body playlistId: PlaylistId
): Message
@POST("user/playlists/create")
suspend fun createPlaylist(
@Header("Authorization") token: String,
@Body name: com.github.libretube.api.obj.Playlists
): com.github.libretube.api.obj.PlaylistId
@Body name: Playlists
): PlaylistId
@POST("user/playlists/add")
suspend fun addToPlaylist(
@Header("Authorization") token: String,
@Body playlistId: com.github.libretube.api.obj.PlaylistId
): com.github.libretube.api.obj.Message
@Body playlistId: PlaylistId
): Message
@POST("user/playlists/remove")
suspend fun removeFromPlaylist(
@Header("Authorization") token: String,
@Body playlistId: com.github.libretube.api.obj.PlaylistId
): com.github.libretube.api.obj.Message
@Body playlistId: PlaylistId
): Message
}

View File

@ -0,0 +1,187 @@
package com.github.libretube.api
import android.content.Context
import android.util.Log
import com.github.libretube.R
import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.obj.Playlists
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toLocalPlaylistItem
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ProxyHelper
import retrofit2.HttpException
import java.io.IOException
object PlaylistsHelper {
private val pipedPlaylistRegex = "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}".toRegex()
val token get() = PreferenceHelper.getToken()
private fun loggedIn() = token != ""
suspend fun getPlaylists(): List<Playlists> {
if (loggedIn()) return RetrofitInstance.authApi.getUserPlaylists(token)
val localPlaylists = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}
val playlists = mutableListOf<Playlists>()
localPlaylists.forEach {
playlists.add(
Playlists(
id = it.playlist.id.toString(),
name = it.playlist.name,
thumbnail = ProxyHelper.rewriteUrl(it.playlist.thumbnailUrl),
videos = it.videos.size.toLong()
)
)
}
return playlists
}
suspend fun getPlaylist(playlistType: PlaylistType, playlistId: String): Playlist {
// load locally stored playlists with the auth api
return when (playlistType) {
PlaylistType.PRIVATE -> RetrofitInstance.authApi.getPlaylist(playlistId)
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
PlaylistType.LOCAL -> {
val relation = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }
return Playlist(
name = relation.playlist.name,
thumbnailUrl = ProxyHelper.rewriteUrl(relation.playlist.thumbnailUrl),
videos = relation.videos.size,
relatedStreams = relation.videos.map { it.toStreamItem() }
)
}
}
}
suspend fun createPlaylist(playlistName: String, appContext: Context, onSuccess: () -> Unit) {
if (!loggedIn()) {
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().createPlaylist(
LocalPlaylist(
name = playlistName,
thumbnailUrl = ""
)
)
}
onSuccess.invoke()
return
}
val response = try {
RetrofitInstance.authApi.createPlaylist(
token,
Playlists(name = playlistName)
)
} catch (e: IOException) {
appContext.toastFromMainThread(R.string.unknown_error)
return
} catch (e: HttpException) {
Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.server_error)
return
}
if (response.playlistId != null) {
appContext.toastFromMainThread(R.string.playlistCreated)
onSuccess.invoke()
} else {
appContext.toastFromMainThread(R.string.unknown_error)
}
}
suspend fun addToPlaylist(playlistId: String, videoId: String): Boolean {
if (!loggedIn()) {
val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId)
awaitQuery {
// avoid duplicated videos in a playlist
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByVideoId(playlistId, videoId)
// add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
if (localPlaylist.playlist.thumbnailUrl == "") {
// set the new playlist thumbnail URL
localPlaylistItem.thumbnailUrl?.let {
localPlaylist.playlist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(localPlaylist.playlist)
}
}
}
return true
}
return RetrofitInstance.authApi.addToPlaylist(
token,
PlaylistId(playlistId, videoId)
).message == "ok"
}
suspend fun renamePlaylist(playlistId: String, newName: String) {
if (!loggedIn()) {
val playlist = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }.playlist
playlist.name = newName
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
return
}
RetrofitInstance.authApi.renamePlaylist(
token,
PlaylistId(
playlistId = playlistId,
newName = newName
)
)
}
suspend fun removeFromPlaylist(playlistId: String, index: Int) {
if (!loggedIn()) {
val transaction = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(transaction.videos[index])
}
if (transaction.videos.size > 1) return
// remove thumbnail if playlist now empty
awaitQuery {
transaction.playlist.thumbnailUrl = ""
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist)
}
return
}
RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(),
PlaylistId(
playlistId = playlistId,
index = index
)
)
}
fun getPrivateType(): PlaylistType {
return if (loggedIn()) PlaylistType.PRIVATE else PlaylistType.LOCAL
}
fun getPrivateType(playlistId: String): PlaylistType {
if (playlistId.all { it.isDigit() }) return PlaylistType.LOCAL
if (playlistId.matches(pipedPlaylistRegex)) return PlaylistType.PRIVATE
return PlaylistType.PUBLIC
}
}

View File

@ -1,12 +1,18 @@
package com.github.libretube.api
import android.content.Context
import android.util.Log
import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Subscription
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -55,6 +61,24 @@ object SubscriptionHelper {
}
}
fun handleUnsubscribe(context: Context, channelId: String, channelName: String?, onUnsubscribe: () -> Unit) {
if (!PreferenceHelper.getBoolean(PreferenceKeys.CONFIRM_UNSUBSCRIBE, false)) {
unsubscribe(channelId)
onUnsubscribe.invoke()
return
}
MaterialAlertDialogBuilder(context)
.setTitle(R.string.unsubscribe)
.setMessage(context.getString(R.string.confirm_unsubscribe, channelName))
.setPositiveButton(R.string.unsubscribe) { _, _ ->
unsubscribe(channelId)
onUnsubscribe.invoke()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
suspend fun isSubscribed(channelId: String): Boolean? {
if (PreferenceHelper.getToken() != "") {
val isSubscribed = try {
@ -99,7 +123,7 @@ object SubscriptionHelper {
}
}
fun getLocalSubscriptions(): List<LocalSubscription> {
private fun getLocalSubscriptions(): List<LocalSubscription> {
return awaitQuery {
Database.localSubscriptionDao().getAll()
}
@ -107,6 +131,30 @@ object SubscriptionHelper {
fun getFormattedLocalSubscriptions(): String {
val localSubscriptions = getLocalSubscriptions()
return localSubscriptions.map { it.channelId }.joinToString(",")
return localSubscriptions.joinToString(",") { it.channelId }
}
suspend fun getSubscriptions(): List<Subscription> {
return if (PreferenceHelper.getToken() != "") {
RetrofitInstance.authApi.subscriptions(
PreferenceHelper.getToken()
)
} else {
RetrofitInstance.authApi.unauthenticatedSubscriptions(
getFormattedLocalSubscriptions()
)
}
}
suspend fun getFeed(): List<StreamItem> {
return if (PreferenceHelper.getToken() != "") {
RetrofitInstance.authApi.getFeed(
PreferenceHelper.getToken()
)
} else {
RetrofitInstance.authApi.getUnauthenticatedFeed(
getFormattedLocalSubscriptions()
)
}
}
}

View File

@ -12,5 +12,6 @@ data class Channel(
var nextpage: String? = null,
var subscriberCount: Long = 0,
var verified: Boolean = false,
var relatedStreams: List<StreamItem>? = null
var relatedStreams: List<StreamItem>? = listOf(),
var tabs: List<ChannelTab>? = listOf()
)

View File

@ -3,6 +3,7 @@ package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Segments(
val segments: MutableList<Segment> = arrayListOf()
data class ChannelTab(
val name: String? = null,
val data: String? = null
)

View File

@ -0,0 +1,6 @@
package com.github.libretube.api.obj
data class ChannelTabResponse(
val content: List<ContentItem> = listOf(),
val nextpage: String? = null
)

View File

@ -6,5 +6,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
data class CommentsPage(
val comments: MutableList<Comment> = arrayListOf(),
val disabled: Boolean? = null,
val nextpage: String? = ""
val nextpage: String? = null
)

View File

@ -3,7 +3,7 @@ package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class SearchItem(
data class ContentItem(
var url: String? = null,
var thumbnail: String? = null,
var uploaderName: String? = null,

View File

@ -0,0 +1,7 @@
package com.github.libretube.api.obj
data class PipedConfig(
val donationUrl: String? = null,
val statusPageUrl: String? = null,
val imageProxyUrl: String? = null
)

View File

@ -17,5 +17,7 @@ data class PipedStream(
var indexEnd: Int? = null,
var width: Int? = null,
var height: Int? = null,
var fps: Int? = null
var fps: Int? = null,
val audioTrackName: String? = null,
val audioTrackId: String? = null
)

View File

@ -7,5 +7,6 @@ data class Playlists(
var id: String? = null,
var name: String? = null,
var shortDescription: String? = null,
var thumbnail: String? = null
var thumbnail: String? = null,
var videos: Long? = null
)

View File

@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class SearchResult(
val items: MutableList<SearchItem>? = arrayListOf(),
val nextpage: String? = "",
val items: MutableList<ContentItem>? = arrayListOf(),
val nextpage: String? = null,
val suggestion: String? = "",
val corrected: Boolean? = null
)

View File

@ -4,7 +4,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Segment(
val UUID: String? = null,
val actionType: String? = null,
val category: String? = null,
val segment: List<Float>? = arrayListOf()
val description: String? = null,
val locked: Int? = null,
val segment: List<Double> = listOf(),
val userID: String? = null,
val videoDuration: Double? = null,
val votes: Int? = null
)

View File

@ -0,0 +1,10 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class SegmentData(
val hash: String? = null,
val segments: List<Segment> = listOf(),
val videoID: String? = null
)

View File

@ -9,7 +9,6 @@ const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/re
* Links for the about fragment
*/
const val WEBSITE_URL = "https://libre-tube.github.io/"
const val DONATE_URL = "https://github.com/libre-tube/LibreTube#donate"
const val GITHUB_URL = "https://github.com/libre-tube/LibreTube"
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped"
const val WEBLATE_URL = "https://hosted.weblate.org/projects/libretube/libretube/"
@ -27,7 +26,7 @@ const val TWITTER_URL = "https://twitter.com/libretube"
/**
* Share Dialog
*/
const val PIPED_FRONTEND_URL = "https://piped.kavin.rocks"
const val PIPED_FRONTEND_URL = "https://piped.video"
const val YOUTUBE_FRONTEND_URL = "https://www.youtube.com"
/**

View File

@ -1,11 +0,0 @@
package com.github.libretube.constants
/**
* object for saving the download type
*/
object DownloadType {
const val AUDIO = 0
const val VIDEO = 1
const val AUDIO_VIDEO = 2
const val NONE = 3
}

View File

@ -8,4 +8,6 @@ object IntentData {
const val timeStamp = "timeStamp"
const val position = "position"
const val fileName = "fileName"
const val openQueueOnce = "openQueue"
const val playlistType = "playlistType"
}

View File

@ -10,6 +10,7 @@ object PreferenceKeys {
const val AUTH_PREF_FILE = "auth"
const val TOKEN = "token"
const val USERNAME = "username"
const val IMAGE_PROXY_URL = "image_proxy_url"
/**
* General
@ -20,7 +21,7 @@ object PreferenceKeys {
const val BREAK_REMINDER_TOGGLE = "break_reminder_toggle"
const val BREAK_REMINDER = "break_reminder"
const val SAVE_FEED = "save_feed"
const val NAVBAR_ITEMS = "nav_bar_items"
const val NAVBAR_ITEMS = "navbar_items"
/**
* Appearance
@ -33,7 +34,7 @@ object PreferenceKeys {
const val APP_ICON = "icon_change"
const val LEGACY_SUBSCRIPTIONS = "legacy_subscriptions"
const val LEGACY_SUBSCRIPTIONS_COLUMNS = "legacy_subscriptions_columns"
const val ALTERNATIVE_TRENDING_LAYOUT = "trending_layout"
const val ALTERNATIVE_VIDEOS_LAYOUT = "alternative_videos_layout"
const val NEW_VIDEOS_BADGE = "new_videos_badge"
const val PLAYLISTS_ORDER = "playlists_order"
@ -77,13 +78,16 @@ object PreferenceKeys {
const val PICTURE_IN_PICTURE = "picture_in_picture"
const val PLAYER_RESIZE_MODE = "player_resize_mode"
const val SB_SKIP_MANUALLY = "sb_skip_manually_key"
const val LIMIT_HLS = "limit_hls"
const val PROGRESSIVE_LOADING_INTERVAL_SIZE = "progressive_loading_interval"
const val SB_SHOW_MARKERS = "sb_show_markers"
const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout"
const val USE_HLS_OVER_DASH = "use_hls"
const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos"
/**
* Background mode
*/
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
const val NOTIFICATION_OPEN_QUEUE = "notification_open_queue"
/**
* Notifications
@ -92,6 +96,10 @@ object PreferenceKeys {
const val CHECKING_FREQUENCY = "checking_frequency"
const val REQUIRED_NETWORK = "required_network"
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
const val IGNORED_NOTIFICATION_CHANNELS = "ignored_notification_channels"
const val NOTIFICATION_TIME_ENABLED = "notification_time"
const val NOTIFICATION_START_TIME = "notification_start_time"
const val NOTIFICATION_END_TIME = "notification_end_time"
/**
* Advanced
@ -103,6 +111,8 @@ object PreferenceKeys {
const val CLEAR_WATCH_HISTORY = "clear_watch_history"
const val CLEAR_WATCH_POSITIONS = "clear_watch_positions"
const val SHARE_WITH_TIME_CODE = "share_with_time_code"
const val CONFIRM_UNSUBSCRIBE = "confirm_unsubscribing"
const val CLEAR_BOOKMARKS = "clear_bookmarks"
/**
* History

View File

@ -1,7 +0,0 @@
package com.github.libretube.constants
object ShareObjectType {
const val VIDEO = 0
const val PLAYLIST = 1
const val CHANNEL = 2
}

View File

@ -1,14 +1,20 @@
package com.github.libretube.db
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.libretube.db.dao.CustomInstanceDao
import com.github.libretube.db.dao.LocalPlaylistsDao
import com.github.libretube.db.dao.LocalSubscriptionDao
import com.github.libretube.db.dao.PlaylistBookmarkDao
import com.github.libretube.db.dao.SearchHistoryDao
import com.github.libretube.db.dao.WatchHistoryDao
import com.github.libretube.db.dao.WatchPositionDao
import com.github.libretube.db.obj.CustomInstance
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.db.obj.LocalPlaylistItem
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition
@ -19,9 +25,16 @@ import com.github.libretube.db.obj.WatchPosition
WatchPosition::class,
SearchHistoryItem::class,
CustomInstance::class,
LocalSubscription::class
LocalSubscription::class,
PlaylistBookmark::class,
LocalPlaylist::class,
LocalPlaylistItem::class
],
version = 7
version = 9,
autoMigrations = [
AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9)
]
)
abstract class AppDatabase : RoomDatabase() {
/**
@ -48,4 +61,14 @@ abstract class AppDatabase : RoomDatabase() {
* Local Subscriptions
*/
abstract fun localSubscriptionDao(): LocalSubscriptionDao
/**
* Bookmarked Playlists
*/
abstract fun playlistBookmarkDao(): PlaylistBookmarkDao
/**
* Local playlists
*/
abstract fun localPlaylistsDao(): LocalPlaylistsDao
}

View File

@ -5,12 +5,13 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.util.PreferenceHelper
object DatabaseHelper {
private const val MAX_SEARCH_HISTORY_SIZE = 20
fun addToWatchHistory(videoId: String, streams: Streams) {
val watchHistoryItem = WatchHistoryItem(
videoId,
@ -37,40 +38,13 @@ object DatabaseHelper {
}
}
fun removeFromWatchHistory(index: Int) {
query {
Database.watchHistoryDao().delete(
Database.watchHistoryDao().getAll()[index]
)
}
}
fun saveWatchPosition(videoId: String, position: Long) {
val watchPosition = WatchPosition(
videoId,
position
)
query {
Database.watchPositionDao().insertAll(watchPosition)
}
}
fun removeWatchPosition(videoId: String) {
query {
Database.watchPositionDao().findById(videoId)?.let {
Database.watchPositionDao().delete(it)
}
}
}
fun addToSearchHistory(searchHistoryItem: SearchHistoryItem) {
query {
Database.searchHistoryDao().insertAll(searchHistoryItem)
val maxHistorySize = 20
// delete the first watch history entry if the limit is reached
val searchHistory = Database.searchHistoryDao().getAll()
if (searchHistory.size > maxHistorySize) {
if (searchHistory.size > MAX_SEARCH_HISTORY_SIZE) {
Database.searchHistoryDao()
.delete(searchHistory.first())
}

View File

@ -0,0 +1,42 @@
package com.github.libretube.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.db.obj.LocalPlaylistItem
import com.github.libretube.db.obj.LocalPlaylistWithVideos
@Dao
interface LocalPlaylistsDao {
@Transaction
@Query("SELECT * FROM LocalPlaylist")
fun getAll(): List<LocalPlaylistWithVideos>
@Insert
fun createPlaylist(playlist: LocalPlaylist)
@Update
fun updatePlaylist(playlist: LocalPlaylist)
@Delete
fun deletePlaylist(playlist: LocalPlaylist)
@Query("DELETE FROM localPlaylist WHERE id = :playlistId")
fun deletePlaylistById(playlistId: String)
@Insert
fun addPlaylistVideo(playlistVideo: LocalPlaylistItem)
@Delete
fun removePlaylistVideo(playlistVideo: LocalPlaylistItem)
@Query("DELETE FROM localPlaylistItem WHERE playlistId = :playlistId")
fun deletePlaylistItemsByPlaylistId(playlistId: String)
@Query("DELETE FROM localPlaylistItem WHERE playlistId = :playlistId AND videoId = :videoId")
fun deletePlaylistItemsByVideoId(playlistId: String, videoId: String)
}

View File

@ -0,0 +1,32 @@
package com.github.libretube.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.libretube.db.obj.PlaylistBookmark
@Dao
interface PlaylistBookmarkDao {
@Query("SELECT * FROM playlistBookmark")
fun getAll(): List<PlaylistBookmark>
@Query("SELECT * FROM playlistBookmark WHERE playlistId LIKE :playlistId LIMIT 1")
fun findById(playlistId: String): PlaylistBookmark
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg bookmarks: PlaylistBookmark)
@Delete
fun delete(playlistBookmark: PlaylistBookmark)
@Query("DELETE FROM playlistBookmark WHERE playlistId = :playlistId")
fun deleteById(playlistId: String)
@Query("SELECT EXISTS(SELECT * FROM playlistBookmark WHERE playlistId= :playlistId)")
fun includes(playlistId: String): Boolean
@Query("DELETE FROM playlistBookmark")
fun deleteAll()
}

View File

@ -21,6 +21,9 @@ interface WatchPositionDao {
@Delete
fun delete(watchPosition: WatchPosition)
@Query("DELETE FROM watchHistoryItem WHERE videoId = :videoId")
fun deleteById(videoId: String)
@Query("DELETE FROM watchPosition")
fun deleteAll()
}

View File

@ -0,0 +1,12 @@
package com.github.libretube.db.obj
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class LocalPlaylist(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
var name: String,
var thumbnailUrl: String
)

View File

@ -0,0 +1,19 @@
package com.github.libretube.db.obj
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class LocalPlaylistItem(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo var playlistId: Int,
@ColumnInfo val videoId: String,
@ColumnInfo val title: String? = null,
@ColumnInfo val uploadDate: String? = null,
@ColumnInfo val uploader: String? = null,
@ColumnInfo val uploaderUrl: String? = null,
@ColumnInfo val uploaderAvatar: String? = null,
@ColumnInfo val thumbnailUrl: String? = null,
@ColumnInfo val duration: Long? = null
)

View File

@ -0,0 +1,13 @@
package com.github.libretube.db.obj
import androidx.room.Embedded
import androidx.room.Relation
data class LocalPlaylistWithVideos(
@Embedded val playlist: LocalPlaylist,
@Relation(
parentColumn = "id",
entityColumn = "playlistId"
)
val videos: List<LocalPlaylistItem>
)

View File

@ -0,0 +1,16 @@
package com.github.libretube.db.obj
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "playlistBookmark")
data class PlaylistBookmark(
@PrimaryKey
val playlistId: String = "",
val playlistName: String? = null,
var thumbnailUrl: String? = null,
var uploader: String? = null,
var uploaderUrl: String? = null,
var uploaderAvatar: String? = null
)

View File

@ -11,7 +11,7 @@ data class WatchHistoryItem(
@ColumnInfo val uploadDate: String? = null,
@ColumnInfo val uploader: String? = null,
@ColumnInfo val uploaderUrl: String? = null,
@ColumnInfo val uploaderAvatar: String? = null,
@ColumnInfo val thumbnailUrl: String? = null,
@ColumnInfo var uploaderAvatar: String? = null,
@ColumnInfo var thumbnailUrl: String? = null,
@ColumnInfo val duration: Long? = null
)

View File

@ -0,0 +1,11 @@
package com.github.libretube.enums
/**
* object for saving the download type
*/
enum class DownloadType {
AUDIO,
VIDEO,
AUDIO_VIDEO,
NONE
}

View File

@ -0,0 +1,18 @@
package com.github.libretube.enums
enum class PlaylistType {
/**
* Local playlist
*/
LOCAL,
/**
* Piped playlist
*/
PRIVATE,
/**
* YouTube playlist
*/
PUBLIC
}

View File

@ -0,0 +1,7 @@
package com.github.libretube.enums
enum class ShareObjectType {
VIDEO,
PLAYLIST,
CHANNEL
}

View File

@ -1,7 +0,0 @@
package com.github.libretube.extensions
import java.io.File
fun File.createDir() = apply {
if (!this.exists()) this.mkdirs()
}

View File

@ -3,15 +3,17 @@ package com.github.libretube.extensions
import java.math.BigDecimal
import java.math.RoundingMode
@Suppress("KotlinConstantConditions")
fun Long?.formatShort(): String = when {
this!! < 1000 -> {
this == null -> (0).toString()
this < 1000 -> {
this.toString()
}
this in 1000..999999 -> {
this in (1000..999999) -> {
val decimal = BigDecimal(this / 1000).setScale(0, RoundingMode.HALF_EVEN)
decimal.toString() + "K"
}
this in 1000000..10000000 -> {
this in (1000000..10000000) -> {
val decimal = BigDecimal(this / 1000000).setScale(0, RoundingMode.HALF_EVEN)
decimal.toString() + "M"
}

View File

@ -0,0 +1,7 @@
package com.github.libretube.extensions
fun <T> MutableList<T>.move(oldPosition: Int, newPosition: Int) {
val item = this.get(oldPosition)
this.removeAt(oldPosition)
this.add(newPosition, item)
}

View File

@ -0,0 +1,9 @@
package com.github.libretube.extensions
import kotlin.math.pow
import kotlin.math.roundToInt
fun Float.round(decimalPlaces: Int): Float {
return (this * 10.0.pow(decimalPlaces.toDouble())).roundToInt() / 10.0.pow(decimalPlaces.toDouble())
.toFloat()
}

View File

@ -0,0 +1,13 @@
package com.github.libretube.ui.extensions
import android.os.Build
import android.os.Bundle
import java.io.Serializable
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> {
@Suppress("DEPRECATION")
getSerializable(key) as? T
}
}

View File

@ -3,9 +3,8 @@ package com.github.libretube.extensions
/**
* format a Piped route to an ID
*/
fun Any.toID(): String {
fun String.toID(): String {
return this
.toString()
.replace("/watch?v=", "") // videos
.replace("/channel/", "") // channels
.replace("/playlist?list=", "") // playlists

View File

@ -0,0 +1,18 @@
package com.github.libretube.extensions
import com.github.libretube.api.obj.Streams
import com.github.libretube.db.obj.LocalPlaylistItem
fun Streams.toLocalPlaylistItem(playlistId: String, videoId: String): LocalPlaylistItem {
return LocalPlaylistItem(
playlistId = playlistId.toInt(),
videoId = videoId,
title = title,
thumbnailUrl = thumbnailUrl,
uploader = uploader,
uploaderUrl = uploaderUrl,
uploaderAvatar = uploaderAvatar,
uploadDate = uploadDate,
duration = duration
)
}

View File

@ -0,0 +1,37 @@
package com.github.libretube.extensions
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams
import com.github.libretube.db.obj.LocalPlaylistItem
import com.github.libretube.util.ProxyHelper
fun Streams.toStreamItem(videoId: String): StreamItem {
return StreamItem(
url = videoId,
title = title,
thumbnail = thumbnailUrl,
uploaderName = uploader,
uploaderUrl = uploaderUrl,
uploaderAvatar = uploaderAvatar,
uploadedDate = uploadDate,
uploaded = null,
duration = duration,
views = views,
uploaderVerified = uploaderVerified,
shortDescription = description
)
}
fun LocalPlaylistItem.toStreamItem(): StreamItem {
return StreamItem(
url = videoId,
title = title,
thumbnail = ProxyHelper.rewriteUrl(thumbnailUrl),
uploaderName = uploader,
uploaderUrl = uploaderUrl,
uploaderAvatar = ProxyHelper.rewriteUrl(uploaderAvatar),
uploadedDate = uploadDate,
uploaded = null,
duration = duration
)
}

View File

@ -0,0 +1,20 @@
package com.github.libretube.extensions
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.widget.Toast
fun Context.toastFromMainThread(text: String) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this,
text,
Toast.LENGTH_SHORT
).show()
}
}
fun Context.toastFromMainThread(stringId: Int) {
toastFromMainThread(getString(stringId))
}

View File

@ -1,5 +0,0 @@
package com.github.libretube.models.interfaces
interface DoubleTapInterface {
fun onEvent(x: Float)
}

View File

@ -1,7 +0,0 @@
package com.github.libretube.models.interfaces
interface PlayerOptionsInterface {
fun onCaptionClicked()
fun onQualityClicked()
}

View File

@ -1,7 +1,9 @@
package com.github.libretube.obj
import com.github.libretube.db.obj.CustomInstance
import com.github.libretube.db.obj.LocalPlaylistWithVideos
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition
@ -12,5 +14,7 @@ data class BackupFile(
var searchHistory: List<SearchHistoryItem>? = null,
var localSubscriptions: List<LocalSubscription>? = null,
var customInstances: List<CustomInstance>? = null,
var playlistBookmarks: List<PlaylistBookmark>? = null,
var localPlaylists: List<LocalPlaylistWithVideos>? = null,
var preferences: List<PreferenceItem>? = null
)

View File

@ -3,5 +3,6 @@ package com.github.libretube.obj
data class BottomSheetItem(
val title: String,
val drawable: Int? = null,
val currentValue: String? = null
val getCurrent: () -> String? = { null },
val onClick: () -> Unit = {}
)

View File

@ -0,0 +1,14 @@
package com.github.libretube.obj
import androidx.annotation.IdRes
import com.github.libretube.R
sealed class ChannelTabs(
val identifierName: String,
@IdRes val chipId: Int
) {
object Playlists : ChannelTabs("playlists", R.id.playlists)
object Shorts : ChannelTabs("shorts", R.id.shorts)
object Livestreams : ChannelTabs("livestreams", R.id.livestreams)
object Channels : ChannelTabs("channels", R.id.channels)
}

View File

@ -6,7 +6,6 @@ import com.github.libretube.api.obj.Streams
data class DownloadedFile(
val name: String,
val size: Long,
val type: Int,
var metadata: Streams? = null,
var thumbnail: Bitmap? = null
)

View File

@ -1,7 +0,0 @@
package com.github.libretube.obj
data class NavBarItem(
val id: Int = 0,
val title: String = "",
var isEnabled: Boolean = true
)

View File

@ -0,0 +1,8 @@
package com.github.libretube.obj
data class ShareData(
val currentChannel: String? = null,
val currentVideo: String? = null,
val currentPlaylist: String? = null,
var currentPosition: Long? = null
)

View File

@ -0,0 +1,7 @@
package com.github.libretube.obj
data class VideoResolution(
val name: String,
val resolution: Int? = null,
val adaptiveSourceUrl: String? = null
)

View File

@ -9,23 +9,24 @@ import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.widget.Toast
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Segments
import com.github.libretube.api.obj.SegmentData
import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.WatchPosition
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.util.AutoPlayHelper
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayingQueue
@ -53,6 +54,7 @@ class BackgroundMode : Service() {
*PlaylistId for autoplay
*/
private var playlistId: String? = null
private var playlistType: PlaylistType? = null
/**
* The response that gets when called the Api.
@ -73,23 +75,13 @@ class BackgroundMode : Service() {
/**
* SponsorBlock Segment data
*/
private var segmentData: Segments? = null
private var segmentData: SegmentData? = null
/**
* [Notification] for the player
*/
private lateinit var nowPlayingNotification: NowPlayingNotification
/**
* The [videoId] of the next stream for autoplay
*/
private var nextStreamId: String? = null
/**
* Helper for finding the next video in the playlist
*/
private lateinit var autoPlayHelper: AutoPlayHelper
/**
* Autoplay Preference
*/
@ -100,18 +92,21 @@ class BackgroundMode : Service() {
*/
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= 26) {
val channelId = BACKGROUND_CHANNEL_ID
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
BACKGROUND_CHANNEL_ID,
"Background Service",
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val notification: Notification = Notification.Builder(this, channelId)
// see https://developer.android.com/reference/android/app/Service#startForeground(int,%20android.app.Notification)
val notification: Notification = Notification.Builder(this, BACKGROUND_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.playingOnBackground)).build()
.setContentText(getString(R.string.playingOnBackground))
.build()
startForeground(PLAYER_NOTIFICATION_ID, notification)
}
}
@ -122,20 +117,21 @@ class BackgroundMode : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
// clear the playing queue
PlayingQueue.clear()
PlayingQueue.resetToDefaults()
// get the intent arguments
videoId = intent?.getStringExtra(IntentData.videoId)!!
playlistId = intent.getStringExtra(IntentData.playlistId)
val position = intent.getLongExtra(IntentData.position, 0L)
// initialize the playlist autoPlay Helper
autoPlayHelper = AutoPlayHelper(playlistId)
// play the audio in the background
loadAudio(videoId, position)
updateWatchPosition()
PlayingQueue.setOnQueueTapListener { streamItem ->
streamItem.url?.toID()?.let { playNextVideo(it) }
}
if (PlayerHelper.watchPositionsEnabled) updateWatchPosition()
} catch (e: Exception) {
onDestroy()
}
@ -143,7 +139,11 @@ class BackgroundMode : Service() {
}
private fun updateWatchPosition() {
player?.currentPosition?.let { DatabaseHelper.saveWatchPosition(videoId, it) }
player?.currentPosition?.let {
query {
Database.watchPositionDao().insertAll(WatchPosition(videoId, it))
}
}
handler.postDelayed(this::updateWatchPosition, 500)
}
@ -154,8 +154,6 @@ class BackgroundMode : Service() {
videoId: String,
seekToPosition: Long = 0
) {
// append the video to the playing queue
PlayingQueue.add(videoId)
CoroutineScope(Dispatchers.IO).launch {
try {
streams = RetrofitInstance.api.getStreams(videoId)
@ -163,6 +161,21 @@ class BackgroundMode : Service() {
return@launch
}
// add the playlist video to the queue
if (playlistId != null && PlayingQueue.isEmpty()) {
streams?.toStreamItem(videoId)
?.let {
PlayingQueue.insertPlaylist(playlistId!!, it)
}
} else {
streams?.toStreamItem(videoId)?.let {
PlayingQueue.updateCurrent(it)
}
streams?.relatedStreams?.toTypedArray()?.let {
if (PlayerHelper.autoInsertRelatedVideos) PlayingQueue.add(*it)
}
}
handler.post {
playAudio(seekToPosition)
}
@ -172,8 +185,6 @@ class BackgroundMode : Service() {
private fun playAudio(
seekToPosition: Long
) {
PlayingQueue.updateCurrent(videoId)
initializePlayer()
setMediaItem()
@ -194,9 +205,8 @@ class BackgroundMode : Service() {
} else if (PlayerHelper.watchPositionsEnabled) {
try {
val watchPosition = awaitQuery {
DatabaseHolder.Database.watchPositionDao().findById(videoId)
Database.watchPositionDao().findById(videoId)
}
Log.e("position", watchPosition.toString())
streams?.duration?.let {
if (watchPosition != null && watchPosition.position < it * 1000 * 0.9) {
player?.seekTo(watchPosition.position)
@ -215,8 +225,6 @@ class BackgroundMode : Service() {
player?.setPlaybackSpeed(playbackSpeed)
fetchSponsorBlockSegments()
if (PlayerHelper.autoPlayEnabled) setNextStream()
}
/**
@ -265,32 +273,16 @@ class BackgroundMode : Service() {
})
}
/**
* set the videoId of the next stream for autoplay
*/
private fun setNextStream() {
if (streams!!.relatedStreams!!.isNotEmpty()) {
nextStreamId = streams?.relatedStreams!![0].url!!.toID()
}
if (playlistId == null) return
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
// search for the next videoId in the playlist
CoroutineScope(Dispatchers.IO).launch {
nextStreamId = autoPlayHelper.getNextVideoId(videoId, streams!!.relatedStreams!!)
}
}
/**
* Plays the first related video to the current (used when the playback of the current video ended)
*/
private fun playNextVideo() {
if (nextStreamId == null || nextStreamId == videoId) return
val nextQueueVideo = PlayingQueue.getNext()
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
private fun playNextVideo(nextId: String? = null) {
val nextVideo = nextId ?: PlayingQueue.getNext()
// play new video on background
this.videoId = nextStreamId!!
if (nextVideo != null) {
this.videoId = nextVideo
}
this.segmentData = null
loadAudio(videoId)
}
@ -322,16 +314,15 @@ class BackgroundMode : Service() {
*/
private fun fetchSponsorBlockSegments() {
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
runCatching {
val categories = PlayerHelper.getSponsorBlockCategories()
if (categories.size > 0) {
segmentData =
RetrofitInstance.api.getSegments(
videoId,
ObjectMapper().writeValueAsString(categories)
)
checkForSegments()
}
if (categories.isEmpty()) return@runCatching
segmentData =
RetrofitInstance.api.getSegments(
videoId,
ObjectMapper().writeValueAsString(categories)
)
checkForSegments()
}
}
}
@ -345,7 +336,7 @@ class BackgroundMode : Service() {
if (segmentData == null || segmentData!!.segments.isEmpty()) return
segmentData!!.segments.forEach { segment: Segment ->
val segmentStart = (segment.segment!![0] * 1000f).toLong()
val segmentStart = (segment.segment[0] * 1000f).toLong()
val segmentEnd = (segment.segment[1] * 1000f).toLong()
val currentPosition = player?.currentPosition
if (currentPosition in segmentStart until segmentEnd) {
@ -367,7 +358,7 @@ class BackgroundMode : Service() {
*/
override fun onDestroy() {
// clear the playing queue
PlayingQueue.clear()
PlayingQueue.resetToDefaults()
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroySelfAndPlayer()

View File

@ -15,7 +15,7 @@ import com.github.libretube.R
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
import com.github.libretube.constants.DOWNLOAD_FAILURE_NOTIFICATION_ID
import com.github.libretube.constants.DOWNLOAD_SUCCESS_NOTIFICATION_ID
import com.github.libretube.constants.DownloadType
import com.github.libretube.enums.DownloadType
import com.github.libretube.extensions.TAG
import com.github.libretube.util.DownloadHelper
import java.io.File
@ -25,7 +25,7 @@ class DownloadService : Service() {
private lateinit var videoName: String
private lateinit var videoUrl: String
private lateinit var audioUrl: String
private var downloadType: Int = 3
private var downloadType: DownloadType = DownloadType.NONE
private var videoDownloadId: Long? = null
private var audioDownloadId: Long? = null
@ -63,8 +63,8 @@ class DownloadService : Service() {
private fun downloadManager() {
// initialize and create the directories to download into
val videoDownloadDir = DownloadHelper.getVideoDir(this)
val audioDownloadDir = DownloadHelper.getAudioDir(this)
val videoDownloadDir = DownloadHelper.getDownloadDir(this, DownloadHelper.VIDEO_DIR)
val audioDownloadDir = DownloadHelper.getDownloadDir(this, DownloadHelper.AUDIO_DIR)
// start download
try {
@ -74,7 +74,7 @@ class DownloadService : Service() {
)
if (downloadType in listOf(DownloadType.VIDEO, DownloadType.AUDIO_VIDEO)) {
videoDownloadId = downloadManagerRequest(
getString(R.string.video),
"[${getString(R.string.video)}] $videoName",
getString(R.string.downloading),
videoUrl,
Uri.fromFile(
@ -84,7 +84,7 @@ class DownloadService : Service() {
}
if (downloadType in listOf(DownloadType.AUDIO, DownloadType.AUDIO_VIDEO)) {
audioDownloadId = downloadManagerRequest(
getString(R.string.audio),
"[${getString(R.string.audio)}] $videoName",
getString(R.string.downloading),
audioUrl,
Uri.fromFile(

View File

@ -11,6 +11,7 @@ import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import com.github.libretube.R
import com.github.libretube.util.DownloadHelper
import java.io.File
class UpdateService : Service() {
@ -28,9 +29,7 @@ class UpdateService : Service() {
}
private fun downloadApk(downloadUrl: String) {
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
// val dir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
file = File(dir, "release.apk")
file = File(getDownloadDirectory(), "release.apk")
val request: DownloadManager.Request =
DownloadManager.Request(Uri.parse(downloadUrl))
@ -80,6 +79,12 @@ class UpdateService : Service() {
}
}
private fun getDownloadDirectory(): File {
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (!downloadsDir.canWrite()) return DownloadHelper.getOfflineStorageDir(this)
return downloadsDir
}
override fun onDestroy() {
unregisterReceiver(onDownloadComplete)
super.onDestroy()

View File

@ -14,10 +14,10 @@ import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
@ -27,18 +27,20 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.extensions.toID
import com.github.libretube.models.PlayerViewModel
import com.github.libretube.models.SearchViewModel
import com.github.libretube.models.SubscriptionsViewModel
import com.github.libretube.services.ClosingService
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.ErrorDialog
import com.github.libretube.ui.fragments.PlayerFragment
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.ui.tools.BreakReminder
import com.github.libretube.util.NavBarHelper
import com.github.libretube.util.NetworkHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
class MainActivity : BaseActivity() {
@ -47,21 +49,17 @@ class MainActivity : BaseActivity() {
lateinit var navController: NavController
private var startFragmentId = R.id.homeFragment
var autoRotationEnabled = false
val autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
lateinit var searchView: SearchView
lateinit var searchItem: MenuItem
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
// enable auto rotation if turned on
requestedOrientation = if (autoRotationEnabled) {
ActivityInfo.SCREEN_ORIENTATION_USER
} else {
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
requestOrientationChange()
// start service that gets called on closure
try {
@ -93,8 +91,12 @@ class MainActivity : BaseActivity() {
// sets the navigation bar color to the previously calculated color
window.navigationBarColor = color
// save start tab fragment id
startFragmentId = NavBarHelper.applyNavBarStyle(binding.bottomNav)
// save start tab fragment id and apply navbar style
startFragmentId = try {
NavBarHelper.applyNavBarStyle(binding.bottomNav)
} catch (e: Exception) {
R.id.homeFragment
}
// set default tab as start fragment
navController.graph.setStartDestination(startFragmentId)
@ -104,18 +106,16 @@ class MainActivity : BaseActivity() {
binding.bottomNav.setOnApplyWindowInsetsListener(null)
binding.bottomNav.setOnItemSelectedListener {
// clear backstack if it's the start fragment
if (startFragmentId == it.itemId) navController.backQueue.clear()
if (it.itemId == R.id.subscriptionsFragment) {
binding.bottomNav.removeBadge(R.id.subscriptionsFragment)
// Prevent duplicate entries into backstack, if selected item and current
// visible fragment is different, then navigate to selected item.
binding.bottomNav.setOnItemReselectedListener {
if (it.itemId != navController.currentDestination?.id) {
navigateToBottomSelectedItem(it)
}
}
removeSearchFocus()
// navigate to the selected fragment
navController.navigate(it.itemId)
binding.bottomNav.setOnItemSelectedListener {
navigateToBottomSelectedItem(it)
false
}
@ -127,7 +127,7 @@ class MainActivity : BaseActivity() {
val log = PreferenceHelper.getErrorLog()
if (log != "") ErrorDialog().show(supportFragmentManager, null)
setupBreakReminder()
BreakReminder.setupBreakReminder(applicationContext)
setupSubscriptionsBadge()
@ -143,56 +143,38 @@ class MainActivity : BaseActivity() {
}
}
if (navController.currentDestination?.id == startFragmentId) {
moveTaskToBack(true)
} else {
navController.popBackStack()
when (navController.currentDestination?.id) {
startFragmentId -> {
moveTaskToBack(true)
}
R.id.searchResultFragment -> {
navController.popBackStack(R.id.searchFragment, true) ||
navController.popBackStack()
}
else -> {
navController.popBackStack()
}
}
}
})
loadIntentData()
}
/**
* Show a break reminder when watched too long
* Rotate according to the preference
*/
private fun setupBreakReminder() {
if (!PreferenceHelper.getBoolean(
PreferenceKeys.BREAK_REMINDER_TOGGLE,
false
)
) {
return
fun requestOrientationChange() {
requestedOrientation = if (autoRotationEnabled) {
ActivityInfo.SCREEN_ORIENTATION_USER
} else {
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
val breakReminderPref = PreferenceHelper.getString(
PreferenceKeys.BREAK_REMINDER,
"0"
)
if (!breakReminderPref.all { Character.isDigit(it) } ||
breakReminderPref == "" || breakReminderPref == "0"
) {
return
}
Handler(Looper.getMainLooper()).postDelayed(
{
try {
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.share_with_time))
.setMessage(
getString(
R.string.already_spent_time,
breakReminderPref
)
)
.setPositiveButton(R.string.okay, null)
.show()
} catch (e: Exception) {
kotlin.runCatching {
Toast.makeText(this, R.string.take_a_break, Toast.LENGTH_LONG).show()
}
}
},
breakReminderPref.toLong() * 60 * 1000
)
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.action_queue)?.isVisible = PlayingQueue.isNotEmpty()
return super.onPrepareOptionsMenu(menu)
}
/**
@ -227,6 +209,8 @@ class MainActivity : BaseActivity() {
private fun removeSearchFocus() {
searchView.setQuery("", false)
searchView.clearFocus()
searchView.isIconified = true
searchItem.collapseActionView()
searchView.onActionViewCollapsed()
}
@ -236,27 +220,31 @@ class MainActivity : BaseActivity() {
// stuff for the search in the topBar
val searchItem = menu.findItem(R.id.action_search)
this.searchItem = searchItem
searchView = searchItem.actionView as SearchView
val searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
searchView.setOnSearchClickListener {
if (navController.currentDestination?.id != R.id.searchResultFragment) {
searchViewModel.setQuery(null)
navController.navigate(R.id.searchFragment)
}
}
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
val bundle = Bundle()
bundle.putString("query", query)
navController.navigate(R.id.searchResultFragment, bundle)
searchViewModel.setQuery("")
searchView.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
// Prevent navigation when search view is collapsed
if (searchView.isIconified ||
binding.bottomNav.menu.children.any {
it.itemId == navController.currentDestination?.id
}
) {
return true
}
// prevent malicious navigation when the search view is getting collapsed
if (navController.currentDestination?.id in listOf(
R.id.searchResultFragment,
@ -279,6 +267,36 @@ class MainActivity : BaseActivity() {
return true
}
})
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
if (navController.currentDestination?.id != R.id.searchResultFragment) {
searchViewModel.setQuery(null)
navController.navigate(R.id.searchFragment)
}
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
if (binding.mainMotionLayout.progress == 0F) {
try {
minimizePlayer()
} catch (e: Exception) {
// current fragment isn't the player fragment
}
}
// Handover back press to `BackPressedDispatcher`
else if (binding.bottomNav.menu.children.none {
it.itemId == navController.currentDestination?.id
}
) {
this@MainActivity.onBackPressedDispatcher.onBackPressed()
}
return true
}
})
return super.onCreateOptionsMenu(menu)
}
@ -302,16 +320,14 @@ class MainActivity : BaseActivity() {
startActivity(communityIntent)
true
}
R.id.action_queue -> {
PlayingQueueSheet().show(supportFragmentManager, null)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onStart() {
super.onStart()
// check whether an URI got submitted over the intent data and load it
loadIntentData()
}
private fun loadIntentData() {
intent?.getStringExtra(IntentData.channelId)?.let {
navController.navigate(
@ -334,6 +350,20 @@ class MainActivity : BaseActivity() {
intent?.getStringExtra(IntentData.videoId)?.let {
loadVideo(it, intent?.getLongExtra(IntentData.timeStamp, 0L))
}
when (intent?.getStringExtra("fragmentToOpen")) {
"home" ->
navController.navigate(R.id.homeFragment)
"trends" ->
navController.navigate(R.id.trendsFragment)
"subscriptions" ->
navController.navigate(R.id.subscriptionsFragment)
"library" ->
navController.navigate(R.id.libraryFragment)
}
if (intent?.getBooleanExtra(IntentData.openQueueOnce, false) == true) {
PlayingQueueSheet()
.show(supportFragmentManager)
}
}
private fun loadVideo(videoId: String, timeStamp: Long?) {
@ -359,7 +389,7 @@ class MainActivity : BaseActivity() {
transitionToStart()
}
}
}, 100)
}, 300)
}
private fun minimizePlayer() {
@ -459,6 +489,26 @@ class MainActivity : BaseActivity() {
}
}
private fun navigateToBottomSelectedItem(item: MenuItem) {
// clear backstack if it's the start fragment
if (startFragmentId == item.itemId) navController.backQueue.clear()
if (item.itemId == R.id.subscriptionsFragment) {
binding.bottomNav.removeBadge(R.id.subscriptionsFragment)
}
// navigate to the selected fragment, if the fragment already
// exists in backstack then pop up to that entry
if (!navController.popBackStack(item.itemId, false)) {
navController.navigate(item.itemId)
}
// Remove focus from search view when navigating to bottom view.
// Call only after navigate to destination, so it can be used in
// onMenuItemActionCollapse for backstack management
removeSearchFocus()
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
supportFragmentManager.fragments.forEach { fragment ->

View File

@ -68,28 +68,26 @@ class OfflinePlayerActivity : BaseActivity() {
}
binding.player.initialize(
supportFragmentManager,
null,
binding.doubleTapOverlay.binding,
null
)
}
private fun File.toUri(): Uri? {
return if (this.exists()) Uri.fromFile(this) else null
}
private fun playVideo() {
val videoDownloadDir = DownloadHelper.getVideoDir(this)
val videoFile = File(
videoDownloadDir,
val videoUri = File(
DownloadHelper.getDownloadDir(this, DownloadHelper.VIDEO_DIR),
fileName
)
).toUri()
val audioDownloadDir = DownloadHelper.getAudioDir(this)
val audioFile = File(
audioDownloadDir,
val audioUri = File(
DownloadHelper.getDownloadDir(this, DownloadHelper.AUDIO_DIR),
fileName
)
val videoUri = if (videoFile.exists()) Uri.fromFile(videoFile) else null
val audioUri = if (audioFile.exists()) Uri.fromFile(audioFile) else null
).toUri()
setMediaSource(
videoUri,

View File

@ -10,6 +10,7 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.extensions.TAG
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.util.NavigationHelper
import kotlin.time.Duration
class RouterActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -75,14 +76,14 @@ class RouterActivity : BaseActivity() {
intent.putExtra(IntentData.videoId, videoId)
uri.getQueryParameter("t")
?.let { intent.putExtra(IntentData.timeStamp, it.toLong()) }
?.let { intent.putExtra(IntentData.timeStamp, parseTimestamp(it)) }
}
else -> {
val videoId = uri.path!!.replace("/", "")
intent.putExtra(IntentData.videoId, videoId)
uri.getQueryParameter("t")
?.let { intent.putExtra(IntentData.timeStamp, it.toLong()) }
?.let { intent.putExtra(IntentData.timeStamp, parseTimestamp(it)) }
}
}
return intent
@ -99,4 +100,12 @@ class RouterActivity : BaseActivity() {
)
this.finishAndRemoveTask()
}
private fun parseTimestamp(t: String): Long? {
if (t.all { c -> c.isDigit() }) {
return t.toLong()
}
return Duration.parseOrNull(t)?.inWholeSeconds
}
}

View File

@ -24,16 +24,17 @@ class BottomSheetAdapter(
override fun onBindViewHolder(holder: BottomSheetViewHolder, position: Int) {
val item = items[position]
holder.binding.apply {
val current = item.getCurrent()
title.text =
if (item.currentValue != null) "${item.title} (${item.currentValue})" else item.title
if (current != null) "${item.title} ($current)" else item.title
if (item.drawable != null) {
drawable.setImageResource(item.drawable)
} else {
drawable.visibility =
View.GONE
drawable.visibility = View.GONE
}
root.setOnClickListener {
item.onClick.invoke()
listener.invoke(position)
}
}

View File

@ -1,78 +0,0 @@
package com.github.libretube.ui.adapters
import android.annotation.SuppressLint
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.setWatchProgressLength
import com.github.libretube.extensions.toID
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.ChannelViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class ChannelAdapter(
private val videoFeed: MutableList<StreamItem>,
private val childFragmentManager: FragmentManager,
private val showChannelInfo: Boolean = false
) :
RecyclerView.Adapter<ChannelViewHolder>() {
override fun getItemCount(): Int {
return videoFeed.size
}
fun updateItems(newItems: List<StreamItem>) {
val feedSize = videoFeed.size
videoFeed.addAll(newItems)
notifyItemRangeInserted(feedSize, newItems.size)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = VideoRowBinding.inflate(layoutInflater, parent, false)
return ChannelViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ChannelViewHolder, position: Int) {
val video = videoFeed[position]
holder.binding.apply {
videoTitle.text = video.title
videoInfo.text =
video.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) +
"" + DateUtils.getRelativeTimeSpanString(video.uploaded!!)
thumbnailDuration.text =
DateUtils.formatElapsedTime(video.duration!!)
ImageHelper.loadImage(video.thumbnail, thumbnail)
if (showChannelInfo) {
ImageHelper.loadImage(video.uploaderAvatar, channelImage)
channelName.text = video.uploaderName
}
root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, video.url)
}
val videoId = video.url!!.toID()
root.setOnLongClickListener {
VideoOptionsBottomSheet(videoId)
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
true
}
watchProgress.setWatchProgressLength(videoId, video.duration!!)
}
}
}

View File

@ -1,9 +1,11 @@
package com.github.libretube.ui.adapters
import android.graphics.Color
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.databinding.ChapterColumnBinding
import com.github.libretube.ui.viewholders.ChaptersViewHolder
import com.github.libretube.util.ImageHelper
@ -11,7 +13,7 @@ import com.github.libretube.util.ThemeHelper
import com.google.android.exoplayer2.ExoPlayer
class ChaptersAdapter(
private val chapters: List<com.github.libretube.api.obj.ChapterSegment>,
private val chapters: List<ChapterSegment>,
private val exoPlayer: ExoPlayer
) : RecyclerView.Adapter<ChaptersViewHolder>() {
private var selectedPosition = 0
@ -27,15 +29,15 @@ class ChaptersAdapter(
holder.binding.apply {
ImageHelper.loadImage(chapter.image, chapterImage)
chapterTitle.text = chapter.title
timeStamp.text = chapter.start?.let { DateUtils.formatElapsedTime(it) }
if (selectedPosition == position) {
// get the color for highlighted controls
val color =
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
chapterLL.setBackgroundColor(color)
val color = if (selectedPosition == position) {
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
} else {
chapterLL.setBackgroundColor(Color.TRANSPARENT)
Color.TRANSPARENT
}
chapterLL.setBackgroundColor(color)
root.setOnClickListener {
updateSelectedPosition(position)
val chapterStart = chapter.start!! * 1000 // s -> ms

View File

@ -5,6 +5,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -19,6 +20,7 @@ import com.github.libretube.ui.viewholders.CommentsViewHolder
import com.github.libretube.util.ClipboardHelper
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.TextUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -60,7 +62,7 @@ class CommentsAdapter(
root.scaleY = 0.9f
}
commentInfos.text = comment.author.toString() + "" + comment.commentedTime.toString()
commentInfos.text = comment.author.toString() + TextUtils.SEPARATOR + comment.commentedTime.toString()
commentText.text = comment.commentText.toString()
ImageHelper.loadImage(comment.thumbnail, commentorImage)
@ -71,8 +73,7 @@ class CommentsAdapter(
if (comment.hearted == true) heartedImageView.visibility = View.VISIBLE
if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE
if ((comment.replyCount ?: -1L) > 0L) {
repliesCount.text =
comment.replyCount?.formatShort()
repliesCount.text = comment.replyCount?.formatShort()
}
commentorImage.setOnClickListener {
@ -89,30 +90,7 @@ class CommentsAdapter(
repliesRecView.adapter = repliesAdapter
if (!isRepliesAdapter && comment.repliesPage != null) {
root.setOnClickListener {
when {
repliesAdapter.itemCount.equals(0) -> {
fetchReplies(comment.repliesPage) {
repliesAdapter.updateItems(it.comments)
if (repliesPage.nextpage == null) {
showMore.visibility = View.GONE
return@fetchReplies
}
showMore.visibility = View.VISIBLE
showMore.setOnClickListener {
if (repliesPage.nextpage == null) {
it.visibility = View.GONE
return@setOnClickListener
}
fetchReplies(
repliesPage.nextpage!!
) {
repliesAdapter.updateItems(repliesPage.comments)
}
}
}
}
else -> repliesAdapter.clear()
}
showMoreReplies(comment.repliesPage, showMore, repliesAdapter)
}
}
@ -124,6 +102,36 @@ class CommentsAdapter(
}
}
private fun showMoreReplies(nextPage: String, showMoreBtn: Button, repliesAdapter: CommentsAdapter) {
when {
repliesAdapter.itemCount.equals(0) -> {
fetchReplies(nextPage) {
repliesAdapter.updateItems(it.comments)
if (repliesPage.nextpage == null) {
showMoreBtn.visibility = View.GONE
return@fetchReplies
}
showMoreBtn.visibility = View.VISIBLE
showMoreBtn.setOnClickListener {
if (repliesPage.nextpage == null) {
it.visibility = View.GONE
return@setOnClickListener
}
fetchReplies(
repliesPage.nextpage!!
) {
repliesAdapter.updateItems(repliesPage.comments)
}
}
}
}
else -> {
repliesAdapter.clear()
showMoreBtn.visibility = View.GONE
}
}
}
override fun getItemCount(): Int {
return comments.size
}

View File

@ -13,6 +13,7 @@ import com.github.libretube.obj.DownloadedFile
import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.github.libretube.util.DownloadHelper
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
@ -39,7 +40,7 @@ class DownloadsAdapter(
uploaderName.text = it.uploader
videoInfo.text = it.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) +
"" + it.uploadDate
TextUtils.SEPARATOR + it.uploadDate
}
thumbnailImage.setImageBitmap(file.thumbnail)
@ -60,8 +61,8 @@ class DownloadsAdapter(
) { _, index ->
when (index) {
0 -> {
val audioDir = DownloadHelper.getAudioDir(root.context)
val videoDir = DownloadHelper.getVideoDir(root.context)
val audioDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.AUDIO_DIR)
val videoDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.VIDEO_DIR)
listOf(audioDir, videoDir).forEach {
val f = File(it, file.name)

View File

@ -0,0 +1,72 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.AppIconItemBinding
import com.github.libretube.ui.viewholders.IconsSheetViewHolder
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
class IconsSheetAdapter : RecyclerView.Adapter<IconsSheetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IconsSheetViewHolder {
val binding = AppIconItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return IconsSheetViewHolder(binding)
}
override fun getItemCount(): Int {
return availableIcons.size
}
override fun onBindViewHolder(holder: IconsSheetViewHolder, position: Int) {
val appIcon = availableIcons[position]
holder.binding.apply {
iconIV.setImageResource(appIcon.iconResource)
iconName.text = root.context.getString(appIcon.nameResource)
root.setOnClickListener {
PreferenceHelper.putString(PreferenceKeys.APP_ICON, appIcon.activityAlias)
ThemeHelper.changeIcon(root.context, appIcon.activityAlias)
}
}
}
companion object {
sealed class AppIcon(
@StringRes val nameResource: Int,
@DrawableRes val iconResource: Int,
val activityAlias: String
) {
object Default :
AppIcon(R.string.defaultIcon, R.mipmap.ic_launcher, "ui.activities.MainActivity")
object DefaultLight :
AppIcon(R.string.defaultIconLight, R.mipmap.ic_launcher_light, "DefaultLight")
object Legacy : AppIcon(R.string.legacyIcon, R.mipmap.ic_legacy, "IconLegacy")
object Gradient :
AppIcon(R.string.gradientIcon, R.mipmap.ic_gradient, "IconGradient")
object Fire : AppIcon(R.string.fireIcon, R.mipmap.ic_fire, "IconFire")
object Torch : AppIcon(R.string.torchIcon, R.mipmap.ic_torch, "IconTorch")
object Shaped : AppIcon(R.string.shapedIcon, R.mipmap.ic_shaped, "IconShaped")
object Flame : AppIcon(R.string.flameIcon, R.mipmap.ic_flame, "IconFlame")
object Bird : AppIcon(R.string.birdIcon, R.mipmap.ic_bird, "IconBird")
}
val availableIcons = listOf(
AppIcon.Default,
AppIcon.DefaultLight,
AppIcon.Legacy,
AppIcon.Gradient,
AppIcon.Fire,
AppIcon.Torch,
AppIcon.Shaped,
AppIcon.Flame,
AppIcon.Bird
)
}
}

View File

@ -1,16 +1,16 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.databinding.NavOptionsItemBinding
import com.github.libretube.obj.NavBarItem
import com.github.libretube.ui.viewholders.NavBarOptionsViewHolder
class NavBarOptionsAdapter(
val items: MutableList<NavBarItem>
val items: MutableList<MenuItem>
) : RecyclerView.Adapter<NavBarOptionsViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NavBarOptionsViewHolder {
@ -30,9 +30,9 @@ class NavBarOptionsAdapter(
val item = items[position]
holder.binding.apply {
title.text = item.title
checkbox.isChecked = item.isEnabled
checkbox.isChecked = item.isVisible
checkbox.setOnClickListener {
if (!checkbox.isChecked && getEnabledItemsCount() < 2) {
if (!checkbox.isChecked && getVisibleItemsCount() < 2) {
checkbox.isChecked = true
Toast.makeText(
root.context,
@ -41,12 +41,12 @@ class NavBarOptionsAdapter(
).show()
return@setOnClickListener
}
items[position].isEnabled = checkbox.isChecked
items[position].isVisible = checkbox.isChecked
}
}
}
private fun getEnabledItemsCount(): Int {
return items.filter { it.isEnabled }.size
private fun getVisibleItemsCount(): Int {
return items.filter { it.isVisible }.size
}
}

View File

@ -0,0 +1,56 @@
package com.github.libretube.ui.adapters
import android.annotation.SuppressLint
import android.graphics.Color
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.QueueRowBinding
import com.github.libretube.ui.viewholders.PlayingQueueViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.ThemeHelper
class PlayingQueueAdapter : RecyclerView.Adapter<PlayingQueueViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlayingQueueViewHolder {
val binding = QueueRowBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PlayingQueueViewHolder(binding)
}
override fun getItemCount(): Int {
return PlayingQueue.size()
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: PlayingQueueViewHolder, position: Int) {
val streamItem = PlayingQueue.getStreams()[position]
holder.binding.apply {
ImageHelper.loadImage(streamItem.thumbnail, thumbnail)
title.text = streamItem.title
videoInfo.text = streamItem.uploaderName + "" +
DateUtils.formatElapsedTime(streamItem.duration ?: 0)
val currentIndex = PlayingQueue.currentIndex()
root.setBackgroundColor(
if (currentIndex == position) {
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
} else {
Color.TRANSPARENT
}
)
root.setOnClickListener {
val oldIndex = PlayingQueue.currentIndex()
PlayingQueue.onQueueItemSelected(position)
notifyItemChanged(oldIndex)
notifyItemChanged(position)
}
}
}
}

View File

@ -1,42 +1,41 @@
package com.github.libretube.ui.adapters
import android.app.Activity
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.databinding.PlaylistRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.extensions.setWatchProgressLength
import com.github.libretube.extensions.toID
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PreferenceHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class PlaylistAdapter(
private val videoFeed: MutableList<com.github.libretube.api.obj.StreamItem>,
private val videoFeed: MutableList<StreamItem>,
private val playlistId: String,
private val isOwner: Boolean,
private val activity: Activity,
private val childFragmentManager: FragmentManager
private val playlistType: PlaylistType
) : RecyclerView.Adapter<PlaylistViewHolder>() {
override fun getItemCount(): Int {
return videoFeed.size
}
fun updateItems(newItems: List<com.github.libretube.api.obj.StreamItem>) {
fun updateItems(newItems: List<StreamItem>) {
val oldSize = videoFeed.size
videoFeed.addAll(newItems)
notifyItemRangeInserted(oldSize, videoFeed.size)
@ -59,40 +58,37 @@ class PlaylistAdapter(
NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)
}
val videoId = streamItem.url!!.toID()
val videoName = streamItem.title!!
root.setOnLongClickListener {
VideoOptionsBottomSheet(videoId)
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
VideoOptionsBottomSheet(videoId, videoName)
.show(
(root.context as BaseActivity).supportFragmentManager,
VideoOptionsBottomSheet::class.java.name
)
true
}
if (isOwner) {
if (playlistType != PlaylistType.PUBLIC) {
deletePlaylist.visibility = View.VISIBLE
deletePlaylist.setOnClickListener {
removeFromPlaylist(position)
removeFromPlaylist(root.context, position)
}
}
watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
}
}
fun removeFromPlaylist(position: Int) {
fun removeFromPlaylist(context: Context, position: Int) {
videoFeed.removeAt(position)
activity.runOnUiThread { notifyDataSetChanged() }
(context as Activity).runOnUiThread {
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
}
CoroutineScope(Dispatchers.IO).launch {
try {
RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(),
com.github.libretube.api.obj.PlaylistId(
playlistId = playlistId,
index = position
)
)
PlaylistsHelper.removeFromPlaylist(playlistId, position)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
return@launch
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Log.e(TAG(), e.toString())
return@launch
}
}

View File

@ -0,0 +1,65 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.PlaylistBookmarkRowBinding
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.toDp
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistBookmarkViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class PlaylistBookmarkAdapter(
private val bookmarks: List<PlaylistBookmark>,
private val bookmarkMode: BookmarkMode = BookmarkMode.FRAGMENT
) : RecyclerView.Adapter<PlaylistBookmarkViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistBookmarkViewHolder {
val binding = PlaylistBookmarkRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PlaylistBookmarkViewHolder(binding)
}
override fun getItemCount(): Int {
return bookmarks.size
}
override fun onBindViewHolder(holder: PlaylistBookmarkViewHolder, position: Int) {
val bookmark = bookmarks[position]
holder.binding.apply {
if (bookmarkMode == BookmarkMode.HOME) {
val params = root.layoutParams
params.width = (210).toDp(root.context.resources).toInt()
root.layoutParams = params
}
ImageHelper.loadImage(bookmark.thumbnailUrl, thumbnail)
playlistName.text = bookmark.playlistName
uploaderName.text = bookmark.uploader
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, bookmark.playlistId, PlaylistType.PUBLIC)
}
root.setOnLongClickListener {
PlaylistOptionsBottomSheet(
playlistId = bookmark.playlistId,
playlistName = bookmark.playlistName ?: "",
playlistType = PlaylistType.PUBLIC
).show(
(root.context as AppCompatActivity).supportFragmentManager
)
true
}
}
}
companion object {
enum class BookmarkMode {
HOME,
FRAGMENT
}
}
}

View File

@ -1,38 +1,29 @@
package com.github.libretube.ui.adapters
import android.app.Activity
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Playlists
import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.enums.PlaylistType
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.DeletePlaylistDialog
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistsViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class PlaylistsAdapter(
private val playlists: MutableList<com.github.libretube.api.obj.Playlists>,
private val childFragmentManager: FragmentManager,
private val activity: Activity
private val playlists: MutableList<Playlists>,
private val playlistType: PlaylistType
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
override fun getItemCount(): Int {
return playlists.size
}
fun updateItems(newItems: List<com.github.libretube.api.obj.Playlists>) {
fun updateItems(newItems: List<Playlists>) {
val oldSize = playlists.size
playlists.addAll(newItems)
notifyItemRangeInserted(oldSize, playlists.size)
@ -55,61 +46,37 @@ class PlaylistsAdapter(
ImageHelper.loadImage(playlist.thumbnail, playlistThumbnail)
}
playlistTitle.text = playlist.name
videoCount.text = playlist.videos.toString()
deletePlaylist.setOnClickListener {
val builder = MaterialAlertDialogBuilder(root.context)
builder.setTitle(R.string.deletePlaylist)
builder.setMessage(R.string.areYouSure)
builder.setPositiveButton(R.string.yes) { _, _ ->
PreferenceHelper.getToken()
deletePlaylist(playlist.id!!, position)
}
builder.setNegativeButton(R.string.cancel, null)
builder.show()
DeletePlaylistDialog(playlist.id!!, playlistType) {
playlists.removeAt(position)
(root.context as BaseActivity).runOnUiThread {
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
}
}.show(
(root.context as BaseActivity).supportFragmentManager,
null
)
}
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, playlist.id, true)
NavigationHelper.navigatePlaylist(root.context, playlist.id, playlistType)
}
root.setOnLongClickListener {
val playlistOptionsDialog = PlaylistOptionsBottomSheet(
playlistId = playlist.id!!,
isOwner = true
playlistName = playlist.name!!,
playlistType = playlistType
)
playlistOptionsDialog.show(
childFragmentManager,
(root.context as BaseActivity).supportFragmentManager,
PlaylistOptionsBottomSheet::class.java.name
)
true
}
}
}
private fun deletePlaylist(id: String, position: Int) {
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.deletePlaylist(
PreferenceHelper.getToken(),
com.github.libretube.api.obj.PlaylistId(id)
)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
return@launch
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
return@launch
}
try {
if (response.message == "ok") {
playlists.removeAt(position)
activity.runOnUiThread {
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
}
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
}
}
}
}

View File

@ -4,34 +4,32 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.SearchItem
import com.github.libretube.api.obj.ContentItem
import com.github.libretube.databinding.ChannelRowBinding
import com.github.libretube.databinding.PlaylistSearchRowBinding
import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.extensions.setWatchProgressLength
import com.github.libretube.extensions.toID
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.SearchViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.github.libretube.util.TextUtils
class SearchAdapter(
private val searchItems: MutableList<SearchItem>,
private val childFragmentManager: FragmentManager
private val searchItems: MutableList<ContentItem>
) :
RecyclerView.Adapter<SearchViewHolder>() {
fun updateItems(newItems: List<SearchItem>) {
fun updateItems(newItems: List<ContentItem>) {
val searchItemsSize = searchItems.size
searchItems.addAll(newItems)
notifyItemRangeInserted(searchItemsSize, newItems.size)
@ -52,7 +50,7 @@ class SearchAdapter(
ChannelRowBinding.inflate(layoutInflater, parent, false)
)
2 -> SearchViewHolder(
PlaylistSearchRowBinding.inflate(layoutInflater, parent, false)
PlaylistsRowBinding.inflate(layoutInflater, parent, false)
)
else -> throw IllegalArgumentException("Invalid type")
}
@ -81,7 +79,7 @@ class SearchAdapter(
}
}
private fun bindWatch(item: SearchItem, binding: VideoRowBinding) {
private fun bindWatch(item: ContentItem, binding: VideoRowBinding) {
binding.apply {
ImageHelper.loadImage(item.thumbnail, thumbnail)
thumbnailDuration.setFormattedDuration(item.duration!!)
@ -100,12 +98,13 @@ class SearchAdapter(
NavigationHelper.navigateVideo(root.context, item.url)
}
val videoId = item.url!!.toID()
val videoName = item.title!!
root.setOnLongClickListener {
VideoOptionsBottomSheet(videoId)
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
VideoOptionsBottomSheet(videoId, videoName)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name)
true
}
channelImage.setOnClickListener {
channelContainer.setOnClickListener {
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
}
watchProgress.setWatchProgressLength(videoId, item.duration!!)
@ -114,7 +113,7 @@ class SearchAdapter(
@SuppressLint("SetTextI18n")
private fun bindChannel(
item: SearchItem,
item: ContentItem,
binding: ChannelRowBinding
) {
binding.apply {
@ -123,66 +122,33 @@ class SearchAdapter(
searchViews.text = root.context.getString(
R.string.subscribers,
item.subscribers.formatShort()
) + "" + root.context.getString(R.string.videoCount, item.videos.toString())
) + TextUtils.SEPARATOR + root.context.getString(R.string.videoCount, item.videos.toString())
root.setOnClickListener {
NavigationHelper.navigateChannel(root.context, item.url)
}
val channelId = item.url!!.toID()
isSubscribed(channelId, binding)
}
}
private fun isSubscribed(channelId: String, binding: ChannelRowBinding) {
// check whether the user subscribed to the channel
CoroutineScope(Dispatchers.Main).launch {
var isSubscribed = SubscriptionHelper.isSubscribed(channelId)
// if subscribed change text to unsubscribe
if (isSubscribed == true) {
binding.searchSubButton.text = binding.root.context.getString(R.string.unsubscribe)
}
// make sub button visible and set the on click listeners to (un)subscribe
if (isSubscribed == null) return@launch
binding.searchSubButton.visibility = View.VISIBLE
binding.searchSubButton.setOnClickListener {
if (isSubscribed == false) {
SubscriptionHelper.subscribe(channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.unsubscribe)
isSubscribed = true
} else {
SubscriptionHelper.unsubscribe(channelId)
binding.searchSubButton.text =
binding.root.context.getString(R.string.subscribe)
isSubscribed = false
}
}
binding.searchSubButton.setupSubscriptionButton(item.url?.toID(), item.name?.toID())
}
}
private fun bindPlaylist(
item: SearchItem,
binding: PlaylistSearchRowBinding
item: ContentItem,
binding: PlaylistsRowBinding
) {
binding.apply {
ImageHelper.loadImage(item.thumbnail, searchThumbnail)
if (item.videos?.toInt() != -1) searchPlaylistNumber.text = item.videos.toString()
searchDescription.text = item.name
searchName.text = item.uploaderName
if (item.videos?.toInt() != -1) {
searchPlaylistVideos.text =
root.context.getString(R.string.videoCount, item.videos.toString())
}
ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
if (item.videos?.toInt() != -1) videoCount.text = item.videos.toString()
playlistTitle.text = item.name
playlistDescription.text = item.uploaderName
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, item.url, false)
NavigationHelper.navigatePlaylist(root.context, item.url, PlaylistType.PUBLIC)
}
deletePlaylist.visibility = View.GONE
root.setOnLongClickListener {
val playlistId = item.url!!.toID()
PlaylistOptionsBottomSheet(playlistId, false)
.show(childFragmentManager, PlaylistOptionsBottomSheet::class.java.name)
val playlistName = item.name!!
PlaylistOptionsBottomSheet(playlistId, playlistName, PlaylistType.PUBLIC)
.show((root.context as BaseActivity).supportFragmentManager, PlaylistOptionsBottomSheet::class.java.name)
true
}
}

View File

@ -30,6 +30,9 @@ class SearchSuggestionsAdapter(
root.setOnClickListener {
searchView.setQuery(suggestion, true)
}
arrow.setOnClickListener {
searchView.setQuery(suggestion, false)
}
}
}
}

View File

@ -3,16 +3,17 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.Subscription
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
import com.github.libretube.extensions.toID
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class SubscriptionChannelAdapter(private val subscriptions: MutableList<com.github.libretube.api.obj.Subscription>) :
RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
class SubscriptionChannelAdapter(
private val subscriptions: MutableList<Subscription>
) : RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
override fun getItemCount(): Int {
return subscriptions.size
@ -27,27 +28,20 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<com.gith
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
val subscription = subscriptions[position]
var subscribed = true
holder.binding.apply {
subscriptionChannelName.text = subscription.name
ImageHelper.loadImage(subscription.avatar, subscriptionChannelImage)
root.setOnClickListener {
NavigationHelper.navigateChannel(root.context, subscription.url)
}
subscriptionSubscribe.setOnClickListener {
val channelId = subscription.url!!.toID()
if (subscribed) {
subscriptionSubscribe.text = root.context.getString(R.string.subscribe)
SubscriptionHelper.unsubscribe(channelId)
subscribed = false
} else {
subscriptionSubscribe.text =
root.context.getString(R.string.unsubscribe)
SubscriptionHelper.subscribe(channelId)
subscribed = true
}
}
subscriptionSubscribe.setupSubscriptionButton(
subscription.url?.toID(),
subscription.name,
notificationBell,
true
)
}
}
}

View File

@ -1,79 +0,0 @@
package com.github.libretube.ui.adapters
import android.annotation.SuppressLint
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.databinding.TrendingRowBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.extensions.setWatchProgressLength
import com.github.libretube.extensions.toID
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.SubscriptionViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import org.chromium.base.ContextUtils.getApplicationContext
class TrendingAdapter(
private val streamItems: List<com.github.libretube.api.obj.StreamItem>,
private val childFragmentManager: FragmentManager,
private val showAllAtOne: Boolean = true
) : RecyclerView.Adapter<SubscriptionViewHolder>() {
var index = 10
override fun getItemCount(): Int {
return if (showAllAtOne) {
streamItems.size
} else if (index >= streamItems.size) {
streamItems.size - 1
} else {
index
}
}
fun updateItems() {
val oldSize = index
index += 10
notifyItemRangeInserted(oldSize, index)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TrendingRowBinding.inflate(layoutInflater, parent, false)
return SubscriptionViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
val trending = streamItems[position]
holder.binding.apply {
textViewTitle.text = trending.title
textViewChannel.text =
trending.uploaderName + "" +
trending.views.formatShort() + " " +
getApplicationContext().resources.getString(R.string.views_placeholder) +
"" + DateUtils.getRelativeTimeSpanString(trending.uploaded!!)
thumbnailDuration.setFormattedDuration(trending.duration!!)
channelImage.setOnClickListener {
NavigationHelper.navigateChannel(root.context, trending.uploaderUrl)
}
ImageHelper.loadImage(trending.thumbnail, thumbnail)
ImageHelper.loadImage(trending.uploaderAvatar, channelImage)
root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, trending.url)
}
val videoId = trending.url!!.toID()
root.setOnLongClickListener {
VideoOptionsBottomSheet(videoId)
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
true
}
watchProgress.setWatchProgressLength(videoId, trending.duration!!)
}
}
}

View File

@ -0,0 +1,195 @@
package com.github.libretube.ui.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.TrendingRowBinding
import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toDp
import com.github.libretube.extensions.toID
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.VideosViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.TextUtils
class VideosAdapter(
private val streamItems: MutableList<StreamItem>,
private val showAllAtOnce: Boolean = true,
private val forceMode: ForceMode = ForceMode.NONE
) : RecyclerView.Adapter<VideosViewHolder>() {
var index = 10
override fun getItemCount(): Int {
return when {
showAllAtOnce -> streamItems.size
index >= streamItems.size -> streamItems.size - 1
else -> index
}
}
fun updateItems() {
val oldSize = index
index += 10
notifyItemRangeInserted(oldSize, index)
}
fun insertItems(newItems: List<StreamItem>) {
val feedSize = streamItems.size
streamItems.addAll(newItems)
notifyItemRangeInserted(feedSize, newItems.size)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideosViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when {
forceMode in listOf(ForceMode.TRENDING, ForceMode.RELATED, ForceMode.HOME) -> VideosViewHolder(TrendingRowBinding.inflate(layoutInflater, parent, false))
forceMode == ForceMode.CHANNEL -> VideosViewHolder(VideoRowBinding.inflate(layoutInflater, parent, false))
PreferenceHelper.getBoolean(
PreferenceKeys.ALTERNATIVE_VIDEOS_LAYOUT,
false
) -> VideosViewHolder(VideoRowBinding.inflate(layoutInflater, parent, false))
else -> VideosViewHolder(TrendingRowBinding.inflate(layoutInflater, parent, false))
}
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VideosViewHolder, position: Int) {
val video = streamItems[position]
// hide the item if there was an extractor error
if (video.title == null) {
holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
return
}
// Trending layout
holder.trendingRowBinding?.apply {
// set a fixed width for better visuals
val params = root.layoutParams
when (forceMode) {
ForceMode.RELATED -> params.width = (180).toDp(root.context.resources).toInt()
ForceMode.HOME -> params.width = (250).toDp(root.context.resources).toInt()
else -> {}
}
root.layoutParams = params
textViewTitle.text = video.title
textViewChannel.text =
video.uploaderName + TextUtils.SEPARATOR +
video.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) +
TextUtils.SEPARATOR + video.uploaded?.let { DateUtils.getRelativeTimeSpanString(it) }
video.duration?.let { thumbnailDuration.setFormattedDuration(it) }
channelImage.setOnClickListener {
NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
}
ImageHelper.loadImage(video.thumbnail, thumbnail)
ImageHelper.loadImage(video.uploaderAvatar, channelImage)
root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, video.url)
}
val videoId = video.url?.toID()
val videoName = video.title
root.setOnLongClickListener {
if (videoId == null || videoName == null) return@setOnLongClickListener true
VideoOptionsBottomSheet(videoId, videoName)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name)
true
}
if (videoId != null) {
watchProgress.setWatchProgressLength(videoId, video.duration ?: 0L)
}
}
// Normal videos row layout
holder.videoRowBinding?.apply {
videoTitle.text = video.title
videoInfo.text =
video.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) +
TextUtils.SEPARATOR + video.uploaded?.let { DateUtils.getRelativeTimeSpanString(it) }
thumbnailDuration.text =
video.duration?.let { DateUtils.formatElapsedTime(it) }
ImageHelper.loadImage(video.thumbnail, thumbnail)
if (forceMode != ForceMode.CHANNEL) {
ImageHelper.loadImage(video.uploaderAvatar, channelImage)
channelName.text = video.uploaderName
channelContainer.setOnClickListener {
NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
}
}
root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, video.url)
}
val videoId = video.url?.toID()
val videoName = video.title
root.setOnLongClickListener {
if (videoId == null || videoName == null) return@setOnLongClickListener true
VideoOptionsBottomSheet(videoId, videoName)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name)
true
}
if (videoId != null) {
watchProgress.setWatchProgressLength(videoId, video.duration ?: 0L)
}
}
}
companion object {
enum class ForceMode {
NONE,
TRENDING,
ROW,
CHANNEL,
RELATED,
HOME
}
fun getLayout(context: Context): LayoutManager {
return if (PreferenceHelper.getBoolean(
PreferenceKeys.ALTERNATIVE_VIDEOS_LAYOUT,
false
)
) {
LinearLayoutManager(context)
} else {
GridLayoutManager(
context,
PreferenceHelper.getString(
PreferenceKeys.GRID_COLUMNS,
context.resources.getInteger(R.integer.grid_items).toString()
).toInt()
)
}
}
}
}

View File

@ -2,26 +2,28 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.WatchHistoryRowBinding
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.extensions.setFormattedDuration
import com.github.libretube.extensions.setWatchProgressLength
import com.github.libretube.extensions.query
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.WatchHistoryViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class WatchHistoryAdapter(
private val watchHistory: MutableList<WatchHistoryItem>,
private val childFragmentManager: FragmentManager
private val watchHistory: MutableList<WatchHistoryItem>
) :
RecyclerView.Adapter<WatchHistoryViewHolder>() {
fun removeFromWatchHistory(position: Int) {
DatabaseHelper.removeFromWatchHistory(position)
query {
DatabaseHolder.Database.watchHistoryDao().delete(watchHistory[position])
}
watchHistory.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
@ -55,8 +57,8 @@ class WatchHistoryAdapter(
NavigationHelper.navigateVideo(root.context, video.videoId)
}
root.setOnLongClickListener {
VideoOptionsBottomSheet(video.videoId)
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
VideoOptionsBottomSheet(video.videoId, video.title!!)
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name)
true
}

View File

@ -1,10 +1,7 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.ArrayAdapter
import android.widget.Toast
@ -13,36 +10,34 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.constants.IntentData
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.databinding.DialogAddtoplaylistBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.models.PlaylistViewModel
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.ui.models.PlaylistViewModel
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class AddToPlaylistDialog : DialogFragment() {
class AddToPlaylistDialog(
private val videoId: String
) : DialogFragment() {
private lateinit var binding: DialogAddtoplaylistBinding
private val viewModel: PlaylistViewModel by activityViewModels()
private lateinit var videoId: String
private lateinit var token: String
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
videoId = arguments?.getString(IntentData.videoId)!!
binding = DialogAddtoplaylistBinding.inflate(layoutInflater)
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
token = PreferenceHelper.getToken()
binding.createPlaylist.setOnClickListener {
CreatePlaylistDialog {
fetchPlaylists()
}.show(childFragmentManager, null)
}
if (token != "") fetchPlaylists()
fetchPlaylists()
return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
@ -52,16 +47,11 @@ class AddToPlaylistDialog : DialogFragment() {
private fun fetchPlaylists() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.playlists(token)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
PlaylistsHelper.getPlaylists()
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
}
if (response.isNotEmpty()) {
val names = response.map { it.name }
@ -75,8 +65,7 @@ class AddToPlaylistDialog : DialogFragment() {
var selectionIndex = 0
response.forEachIndexed { index, playlist ->
if (playlist.id == viewModel.lastSelectedPlaylistId) {
selectionIndex =
index
selectionIndex = index
}
}
binding.playlistsSpinner.setSelection(selectionIndex)
@ -96,38 +85,19 @@ class AddToPlaylistDialog : DialogFragment() {
private fun addToPlaylist(playlistId: String) {
val appContext = context?.applicationContext ?: return
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.addToPlaylist(
token,
PlaylistId(playlistId, videoId)
)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
toastFromMainThread(appContext, R.string.unknown_error)
return@launch
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
toastFromMainThread(appContext, R.string.server_error)
val success = try {
PlaylistsHelper.addToPlaylist(playlistId, videoId)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.unknown_error)
return@launch
}
toastFromMainThread(
appContext,
if (response.message == "ok") R.string.added_to_playlist else R.string.fail
appContext.toastFromMainThread(
if (success) R.string.added_to_playlist else R.string.fail
)
}
}
private fun toastFromMainThread(context: Context, stringId: Int) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(
context,
stringId,
Toast.LENGTH_SHORT
).show()
}
}
private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return
if (!isAdded) return // Fragment not attached to an Activity

View File

@ -2,9 +2,11 @@ package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem
import com.github.libretube.util.PreferenceHelper
@ -13,21 +15,56 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
class BackupDialog(
private val createBackupFile: (BackupFile) -> Unit
) : DialogFragment() {
private val backupFile = BackupFile()
sealed class BackupOption(@StringRes val name: Int, val onSelected: (BackupFile) -> Unit) {
object WatchHistory : BackupOption(R.string.watch_history, onSelected = {
it.watchHistory = Database.watchHistoryDao().getAll()
})
object WatchPositions : BackupOption(R.string.watch_positions, onSelected = {
it.watchPositions = Database.watchPositionDao().getAll()
})
object SearchHistory : BackupOption(R.string.search_history, onSelected = {
it.searchHistory = Database.searchHistoryDao().getAll()
})
object LocalSubscriptions : BackupOption(R.string.local_subscriptions, onSelected = {
it.localSubscriptions = Database.localSubscriptionDao().getAll()
})
object CustomInstances : BackupOption(R.string.backup_customInstances, onSelected = {
it.customInstances = Database.customInstanceDao().getAll()
})
object PlaylistBookmarks : BackupOption(R.string.bookmarks, onSelected = {
it.playlistBookmarks = Database.playlistBookmarkDao().getAll()
})
object LocalPlaylists : BackupOption(R.string.local_playlists, onSelected = {
it.localPlaylists = Database.localPlaylistsDao().getAll()
})
object Preferences : BackupOption(R.string.preferences, onSelected = {
it.preferences = PreferenceHelper.settings.all.map {
PreferenceItem(it.key, it.value)
}
})
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val backupOptionNames = listOf(
R.string.watch_history,
R.string.watch_positions,
R.string.search_history,
R.string.local_subscriptions,
R.string.backup_customInstances,
R.string.preferences
val backupOptions = listOf(
BackupOption.WatchHistory,
BackupOption.WatchPositions,
BackupOption.SearchHistory,
BackupOption.LocalSubscriptions,
BackupOption.CustomInstances,
BackupOption.PlaylistBookmarks,
BackupOption.LocalPlaylists,
BackupOption.Preferences
)
val backupItems = backupOptionNames.map { context?.getString(it)!! }.toTypedArray()
val backupItems = backupOptions.map { context?.getString(it.name)!! }.toTypedArray()
val selected = BooleanArray(backupOptionNames.size) { false }
val selected = BooleanArray(backupOptions.size) { false }
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.backup)
@ -36,39 +73,12 @@ class BackupDialog(
}
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.backup) { _, _ ->
val thread = Thread {
if (selected[0]) {
backupFile.watchHistory =
Database.watchHistoryDao().getAll()
}
if (selected[1]) {
backupFile.watchPositions =
Database.watchPositionDao().getAll()
}
if (selected[2]) {
backupFile.searchHistory =
Database.searchHistoryDao().getAll()
}
if (selected[3]) {
backupFile.localSubscriptions =
Database.localSubscriptionDao().getAll()
}
if (selected[4]) {
backupFile.customInstances =
Database.customInstanceDao().getAll()
}
if (selected[5]) {
backupFile.preferences = PreferenceHelper.settings.all.map {
PreferenceItem(
it.key,
it.value
)
}
val backupFile = BackupFile()
awaitQuery {
backupOptions.forEachIndexed { index, option ->
if (selected[index]) option.onSelected.invoke(backupFile)
}
}
thread.start()
thread.join()
createBackupFile(backupFile)
}
.create()

View File

@ -2,23 +2,18 @@ package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.databinding.DialogCreatePlaylistBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.ui.fragments.LibraryFragment
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
class CreatePlaylistDialog : DialogFragment() {
private var token: String = ""
class CreatePlaylistDialog(
private val onSuccess: () -> Unit = {}
) : DialogFragment() {
private lateinit var binding: DialogCreatePlaylistBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -30,14 +25,17 @@ class CreatePlaylistDialog : DialogFragment() {
dismiss()
}
token = PreferenceHelper.getToken()
binding.createNewPlaylist.setOnClickListener {
// avoid creating the same playlist multiple times by spamming the button
binding.createNewPlaylist.setOnClickListener(null)
val listName = binding.playlistName.text.toString()
if (listName != "") {
createPlaylist(listName)
lifecycleScope.launchWhenCreated {
PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext) {
onSuccess.invoke()
dismiss()
}
}
} else {
Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show()
}
@ -47,38 +45,4 @@ class CreatePlaylistDialog : DialogFragment() {
.setView(binding.root)
.show()
}
private fun createPlaylist(name: String) {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.createPlaylist(
token,
com.github.libretube.api.obj.Playlists(name = name)
)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response $e")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
}
if (response.playlistId != null) {
Toast.makeText(context, R.string.playlistCreated, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, getString(R.string.unknown_error), Toast.LENGTH_SHORT)
.show()
}
// refresh the playlists in the library
try {
val parent = parentFragment as LibraryFragment
parent.fetchPlaylists()
} catch (e: Exception) {
Log.e(TAG(), e.toString())
}
dismiss()
}
}
}

View File

@ -0,0 +1,65 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class DeletePlaylistDialog(
private val playlistId: String,
private val playlistType: PlaylistType,
private val onSuccess: () -> Unit = {}
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.deletePlaylist)
.setMessage(R.string.areYouSure)
.setPositiveButton(R.string.yes) { _, _ ->
PreferenceHelper.getToken()
deletePlaylist()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun deletePlaylist() {
if (playlistType == PlaylistType.LOCAL) {
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId)
}
return
}
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.deletePlaylist(
PreferenceHelper.getToken(),
PlaylistId(playlistId)
)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launch
}
try {
if (response.message == "ok") {
onSuccess.invoke()
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
}
}
}
}

View File

@ -62,7 +62,7 @@ class NavBarOptionsDialog : DialogFragment() {
.setTitle(R.string.navigation_bar)
.setView(binding.root)
.setPositiveButton(R.string.okay) { _, _ ->
NavBarHelper.setNavBarItems(adapter.items)
NavBarHelper.setNavBarItems(adapter.items, requireContext())
RequireRestartDialog()
.show(requireParentFragment().childFragmentManager, null)
}

View File

@ -8,18 +8,19 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.constants.PIPED_FRONTEND_URL
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.constants.ShareObjectType
import com.github.libretube.constants.YOUTUBE_FRONTEND_URL
import com.github.libretube.databinding.DialogShareBinding
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.obj.ShareData
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ShareDialog(
private val id: String,
private val shareObjectType: Int,
private val position: Long? = null
private val shareObjectType: ShareObjectType,
private val shareData: ShareData
) : DialogFragment() {
private var binding: DialogShareBinding? = null
@ -29,11 +30,11 @@ class ShareDialog(
getString(R.string.youtube)
)
val instanceUrl = getCustomInstanceFrontendUrl()
val shareableTitle = getShareableTitle(shareData)
// add instanceUrl option if custom instance frontend url available
if (instanceUrl != "") shareOptions += getString(R.string.instance)
if (shareObjectType == ShareObjectType.VIDEO && position != null) {
if (shareObjectType == ShareObjectType.VIDEO) {
setupTimeStampBinding()
}
@ -55,7 +56,7 @@ class ShareDialog(
}
var url = "$host$path"
if (shareObjectType == ShareObjectType.VIDEO && position != null && binding!!.timeCodeSwitch.isChecked) {
if (shareObjectType == ShareObjectType.VIDEO && binding!!.timeCodeSwitch.isChecked) {
url += "&t=${binding!!.timeStamp.text}"
}
@ -63,6 +64,7 @@ class ShareDialog(
intent.apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, url)
putExtra(Intent.EXTRA_SUBJECT, shareableTitle)
type = "text/plain"
}
context?.startActivity(
@ -81,8 +83,9 @@ class ShareDialog(
)
binding!!.timeCodeSwitch.setOnCheckedChangeListener { _, isChecked ->
binding!!.timeStampLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
PreferenceHelper.putBoolean(PreferenceKeys.SHARE_WITH_TIME_CODE, isChecked)
}
binding!!.timeStamp.setText(position.toString())
binding!!.timeStamp.setText((shareData.currentPosition ?: 0L).toString())
if (binding!!.timeCodeSwitch.isChecked) binding!!.timeStampLayout.visibility = View.VISIBLE
}
@ -104,4 +107,18 @@ class ShareDialog(
}
return ""
}
private fun getShareableTitle(shareData: ShareData): String {
shareData.apply {
currentChannel?.let {
return it
}
currentVideo?.let {
return it
}
currentPlaylist?.let {
return it
}
}
return ""
}
}

View File

@ -1,4 +1,4 @@
package com.github.libretube.extensions
package com.github.libretube.ui.extensions
import android.text.format.DateUtils
import android.widget.TextView

View File

@ -1,9 +1,10 @@
package com.github.libretube.extensions
package com.github.libretube.ui.extensions
import android.view.View
import android.view.ViewTreeObserver
import android.widget.LinearLayout
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.awaitQuery
/**
* shows the already watched time under the video

View File

@ -0,0 +1,18 @@
package com.github.libretube.ui.extensions
import android.util.Log
import com.github.libretube.R
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.button.MaterialButton
fun MaterialButton.setupNotificationBell(channelId: String) {
var isIgnorable = PreferenceHelper.isChannelNotificationIgnorable(channelId)
Log.e(channelId, isIgnorable.toString())
setIconResource(if (isIgnorable) R.drawable.ic_bell else R.drawable.ic_notification)
setOnClickListener {
isIgnorable = !isIgnorable
PreferenceHelper.toggleIgnorableNotificationChannel(channelId)
setIconResource(if (isIgnorable) R.drawable.ic_bell else R.drawable.ic_notification)
}
}

View File

@ -0,0 +1,51 @@
package com.github.libretube.ui.extensions
import android.view.View
import android.widget.TextView
import com.github.libretube.R
import com.github.libretube.api.SubscriptionHelper
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
fun TextView.setupSubscriptionButton(
channelId: String?,
channelName: String?,
notificationBell: MaterialButton? = null,
isSubscribed: Boolean? = null
) {
if (channelId == null) return
var subscribed: Boolean? = false
CoroutineScope(Dispatchers.IO).launch {
subscribed = isSubscribed ?: SubscriptionHelper.isSubscribed(channelId)
withContext(Dispatchers.Main) {
if (subscribed == true) {
this@setupSubscriptionButton.text = context.getString(R.string.unsubscribe)
} else {
notificationBell?.visibility = View.GONE
}
this@setupSubscriptionButton.visibility = View.VISIBLE
}
}
notificationBell?.setupNotificationBell(channelId)
this.setOnClickListener {
if (subscribed == true) {
SubscriptionHelper.handleUnsubscribe(context, channelId, channelName) {
this.text = context.getString(R.string.subscribe)
notificationBell?.visibility = View.GONE
subscribed = false
}
} else {
SubscriptionHelper.subscribe(channelId)
this.text = context.getString(R.string.unsubscribe)
notificationBell?.visibility = View.VISIBLE
subscribed = true
}
}
}

View File

@ -0,0 +1,5 @@
package com.github.libretube.ui.extensions
fun <T> List<T>.withMaxSize(maxSize: Int): List<T> {
return this.filterIndexed { index, _ -> index < maxSize }
}

View File

@ -0,0 +1,41 @@
package com.github.libretube.ui.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import com.github.libretube.databinding.FragmentBookmarksBinding
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
import com.github.libretube.ui.base.BaseFragment
class BookmarksFragment : BaseFragment() {
private lateinit var binding: FragmentBookmarksBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBookmarksBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bookmarks = awaitQuery {
Database.playlistBookmarkDao().getAll()
}
if (bookmarks.isEmpty()) return
binding.bookmarksRV.layoutManager = GridLayoutManager(context, 2)
binding.bookmarksRV.adapter = PlaylistBookmarkAdapter(bookmarks)
binding.bookmarksRV.visibility = View.VISIBLE
binding.emptyBookmarks.visibility = View.GONE
}
}

View File

@ -6,21 +6,30 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.ChannelTab
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.ShareObjectType
import com.github.libretube.databinding.FragmentChannelBinding
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.ui.adapters.ChannelAdapter
import com.github.libretube.obj.ChannelTabs
import com.github.libretube.obj.ShareData
import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.util.ImageHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
@ -29,12 +38,22 @@ class ChannelFragment : BaseFragment() {
private var channelId: String? = null
private var channelName: String? = null
private var nextPage: String? = null
private var channelAdapter: ChannelAdapter? = null
private var channelAdapter: VideosAdapter? = null
private var isLoading = true
private var isSubscribed: Boolean? = false
private var onScrollEnd: () -> Unit = {}
private val scope = CoroutineScope(Dispatchers.IO)
val possibleTabs = listOf(
ChannelTabs.Channels,
ChannelTabs.Playlists,
ChannelTabs.Livestreams,
ChannelTabs.Shorts
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
@ -63,7 +82,9 @@ class ChannelFragment : BaseFragment() {
binding.channelRefresh.isRefreshing = true
fetchChannel()
}
refreshChannel()
binding.channelRefresh.setOnRefreshListener {
refreshChannel()
}
@ -73,11 +94,10 @@ class ChannelFragment : BaseFragment() {
if (binding.channelScrollView.getChildAt(0).bottom
== (binding.channelScrollView.height + binding.channelScrollView.scrollY)
) {
// scroll view is at bottom
if (nextPage != null && !isLoading) {
isLoading = true
binding.channelRefresh.isRefreshing = true
fetchChannelNextPage()
try {
onScrollEnd.invoke()
} catch (e: Exception) {
Log.e("tabs failed", e.toString())
}
}
}
@ -102,30 +122,26 @@ class ChannelFragment : BaseFragment() {
}
// needed if the channel gets loaded by the ID
channelId = response.id
channelName = response.name
val shareData = ShareData(currentChannel = response.name)
onScrollEnd = {
fetchChannelNextPage()
}
// fetch and update the subscription status
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated
runOnUiThread {
if (isSubscribed == true) {
binding.channelSubscribe.text = getString(R.string.unsubscribe)
}
binding.channelSubscribe.setOnClickListener {
binding.channelSubscribe.text = if (isSubscribed == true) {
SubscriptionHelper.unsubscribe(channelId!!)
isSubscribed = false
getString(R.string.subscribe)
} else {
SubscriptionHelper.subscribe(channelId!!)
isSubscribed = true
getString(R.string.unsubscribe)
}
}
binding.channelSubscribe.setupSubscriptionButton(channelId, channelName, binding.notificationBell)
binding.channelShare.setOnClickListener {
val shareDialog = ShareDialog(response.id!!.toID(), ShareObjectType.CHANNEL)
val shareDialog = ShareDialog(
response.id!!.toID(),
ShareObjectType.CHANNEL,
shareData
)
shareDialog.show(childFragmentManager, ShareDialog::class.java.name)
}
}
@ -165,23 +181,82 @@ class ChannelFragment : BaseFragment() {
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
// recyclerview of the videos by the channel
channelAdapter = ChannelAdapter(
response.relatedStreams!!.toMutableList(),
childFragmentManager
channelAdapter = VideosAdapter(
response.relatedStreams.orEmpty().toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.CHANNEL
)
binding.channelRecView.adapter = channelAdapter
}
response.tabs?.let { setupTabs(it) }
}
}
private fun setupTabs(tabs: List<ChannelTab>) {
binding.tabChips.children.forEach { chip ->
val resourceTab = possibleTabs.firstOrNull { it.chipId == chip.id }
resourceTab?.let { resTab ->
if (tabs.any { it.name == resTab.identifierName }) chip.visibility = View.VISIBLE
}
}
binding.tabChips.setOnCheckedStateChangeListener { _, _ ->
when (binding.tabChips.checkedChipId) {
binding.videos.id -> {
binding.channelRecView.adapter = channelAdapter
onScrollEnd = {
fetchChannelNextPage()
}
}
else -> {
possibleTabs.first { binding.tabChips.checkedChipId == it.chipId }.let {
val tab = tabs.first { tab -> tab.name == it.identifierName }
loadTab(tab)
}
}
}
}
}
private fun loadTab(tab: ChannelTab) {
scope.launch {
tab.data ?: return@launch
val response = try {
RetrofitInstance.api.getChannelTab(tab.data)
} catch (e: Exception) {
return@launch
}
val adapter = SearchAdapter(
response.content.toMutableList()
)
runOnUiThread {
binding.channelRecView.adapter = adapter
}
var tabNextPage = response.nextpage
onScrollEnd = {
tabNextPage?.let {
fetchTabNextPage(it, tab, adapter) { nextPage ->
tabNextPage = nextPage
}
}
}
}
}
private fun fetchChannelNextPage() {
fun run() {
if (nextPage == null || isLoading) return
isLoading = true
binding.channelRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
} catch (e: IOException) {
binding.channelRefresh.isRefreshing = false
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
@ -190,11 +265,33 @@ class ChannelFragment : BaseFragment() {
return@launchWhenCreated
}
nextPage = response.nextpage
channelAdapter?.updateItems(response.relatedStreams!!)
channelAdapter?.insertItems(response.relatedStreams.orEmpty())
isLoading = false
binding.channelRefresh.isRefreshing = false
}
}
run()
}
private fun fetchTabNextPage(
nextPage: String,
tab: ChannelTab,
adapter: SearchAdapter,
onNewNextPage: (String?) -> Unit
) {
scope.launch {
val newContent = try {
RetrofitInstance.api.getChannelTab(tab.data ?: "", nextPage)
} catch (e: Exception) {
e.printStackTrace()
null
}
onNewNextPage.invoke(newContent?.nextpage)
runOnUiThread {
newContent?.content?.let {
adapter.updateItems(it)
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More