mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-15 06:40:30 +05:30
Merge branch 'libre-tube:master' into master
This commit is contained in:
commit
e59c251437
67
.github/tg.py
vendored
67
.github/tg.py
vendored
@ -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())
|
||||
|
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
13
app/proguard-rules.pro
vendored
13
app/proguard-rules.pro
vendored
@ -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.** { *; }
|
||||
|
@ -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"
|
||||
|
174
app/schemas/com.github.libretube.db.AppDatabase/7.json
Normal file
174
app/schemas/com.github.libretube.db.AppDatabase/7.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
224
app/schemas/com.github.libretube.db.AppDatabase/8.json
Normal file
224
app/schemas/com.github.libretube.db.AppDatabase/8.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
330
app/schemas/com.github.libretube.db.AppDatabase/9.json
Normal file
330
app/schemas/com.github.libretube.db.AppDatabase/9.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
BIN
app/src/main/ic_launcher_light-playstore.png
Normal file
BIN
app/src/main/ic_launcher_light-playstore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
}
|
||||
|
187
app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt
Normal file
187
app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package com.github.libretube.api.obj
|
||||
|
||||
data class ChannelTabResponse(
|
||||
val content: List<ContentItem> = listOf(),
|
||||
val nextpage: String? = null
|
||||
)
|
@ -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
|
||||
)
|
||||
|
@ -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,
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -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"
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,7 +0,0 @@
|
||||
package com.github.libretube.constants
|
||||
|
||||
object ShareObjectType {
|
||||
const val VIDEO = 0
|
||||
const val PLAYLIST = 1
|
||||
const val CHANNEL = 2
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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>
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
||||
|
11
app/src/main/java/com/github/libretube/enums/DownloadType.kt
Normal file
11
app/src/main/java/com/github/libretube/enums/DownloadType.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package com.github.libretube.enums
|
||||
|
||||
/**
|
||||
* object for saving the download type
|
||||
*/
|
||||
enum class DownloadType {
|
||||
AUDIO,
|
||||
VIDEO,
|
||||
AUDIO_VIDEO,
|
||||
NONE
|
||||
}
|
18
app/src/main/java/com/github/libretube/enums/PlaylistType.kt
Normal file
18
app/src/main/java/com/github/libretube/enums/PlaylistType.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package com.github.libretube.enums
|
||||
|
||||
enum class PlaylistType {
|
||||
/**
|
||||
* Local playlist
|
||||
*/
|
||||
LOCAL,
|
||||
|
||||
/**
|
||||
* Piped playlist
|
||||
*/
|
||||
PRIVATE,
|
||||
|
||||
/**
|
||||
* YouTube playlist
|
||||
*/
|
||||
PUBLIC
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.github.libretube.enums
|
||||
|
||||
enum class ShareObjectType {
|
||||
VIDEO,
|
||||
PLAYLIST,
|
||||
CHANNEL
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.github.libretube.extensions
|
||||
|
||||
import java.io.File
|
||||
|
||||
fun File.createDir() = apply {
|
||||
if (!this.exists()) this.mkdirs()
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package com.github.libretube.models.interfaces
|
||||
|
||||
interface DoubleTapInterface {
|
||||
fun onEvent(x: Float)
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.github.libretube.models.interfaces
|
||||
|
||||
interface PlayerOptionsInterface {
|
||||
fun onCaptionClicked()
|
||||
|
||||
fun onQualityClicked()
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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 = {}
|
||||
)
|
||||
|
14
app/src/main/java/com/github/libretube/obj/ChannelTabs.kt
Normal file
14
app/src/main/java/com/github/libretube/obj/ChannelTabs.kt
Normal 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)
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -1,7 +0,0 @@
|
||||
package com.github.libretube.obj
|
||||
|
||||
data class NavBarItem(
|
||||
val id: Int = 0,
|
||||
val title: String = "",
|
||||
var isEnabled: Boolean = true
|
||||
)
|
8
app/src/main/java/com/github/libretube/obj/ShareData.kt
Normal file
8
app/src/main/java/com/github/libretube/obj/ShareData.kt
Normal 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
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package com.github.libretube.obj
|
||||
|
||||
data class VideoResolution(
|
||||
val name: String,
|
||||
val resolution: Int? = null,
|
||||
val adaptiveSourceUrl: String? = null
|
||||
)
|
@ -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()
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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 ->
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ class SearchSuggestionsAdapter(
|
||||
root.setOnClickListener {
|
||||
searchView.setQuery(suggestion, true)
|
||||
}
|
||||
arrow.setOnClickListener {
|
||||
searchView.setQuery(suggestion, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.github.libretube.extensions
|
||||
package com.github.libretube.ui.extensions
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.widget.TextView
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user