mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-01-06 01:20:29 +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
|
import asyncio
|
||||||
from tgconfig import *
|
from json import load
|
||||||
from json import load
|
from os import listdir
|
||||||
import multiprocessing
|
|
||||||
from os import system
|
from pyrogram import Client
|
||||||
from time import sleep as wait
|
from pyrogram.types import InputMediaDocument
|
||||||
|
from tgconfig import *
|
||||||
def deploy():
|
|
||||||
system(f'./exec --local --api-id={TG_API_ID} --api-hash={TG_API_HASH}')
|
files = listdir()
|
||||||
|
|
||||||
def bot():
|
mediadocuments = [
|
||||||
wait(10)
|
InputMediaDocument(file) for file in files if file.endswith("signed.apk")
|
||||||
f = open('commit.json')
|
]
|
||||||
data = load(f)
|
|
||||||
f.close()
|
with open("commit.json") as f:
|
||||||
|
data = load(f)
|
||||||
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*
|
caption = f"""**Libretube {data['sha'][0:7]} // Alpha**
|
||||||
|
|
||||||
[{data['commit']['message']}]({data['html_url']})
|
<a href="{data['html_url']}">{data['commit']['message']}</a>
|
||||||
|
|
||||||
Signed-off-by: {data['commit']['author']['name']}
|
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')
|
|
||||||
|
async def main():
|
||||||
if __name__ == '__main__':
|
async with Client("libretube", TG_API_ID, TG_API_HASH, bot_token=TG_TOKEN) as app:
|
||||||
multideploy = multiprocessing.Process(target=deploy)
|
await app.send_photo(
|
||||||
multibot = multiprocessing.Process(target=bot)
|
int(TG_POST_ID), "https://libre-tube.github.io/images/Alpha.png", caption
|
||||||
multideploy.start()
|
)
|
||||||
multibot.start()
|
await app.send_media_group(int(TG_POST_ID), mediadocuments)
|
||||||
multideploy.join()
|
|
||||||
multibot.join()
|
|
||||||
|
asyncio.run(main())
|
||||||
|
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
- uses: actions/setup-python@v3
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
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
|
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||||
@ -54,6 +54,18 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
./gradlew assembleDebug
|
./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
|
- name: Upload to Archive
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
@ -61,7 +73,7 @@ jobs:
|
|||||||
echo "GH_REPO = '${{ github.repository }}'" > tgconfig.py
|
echo "GH_REPO = '${{ github.repository }}'" > tgconfig.py
|
||||||
git clone https://github.com/LibreTubeAlpha/Archive archive
|
git clone https://github.com/LibreTubeAlpha/Archive archive
|
||||||
rm -rf archive/*.apk
|
rm -rf archive/*.apk
|
||||||
mv app/build/outputs/apk/debug/*.apk archive/
|
mv app/build/outputs/apk/debug/*-signed.apk archive/
|
||||||
cd archive
|
cd archive
|
||||||
python ../uploader.py
|
python ../uploader.py
|
||||||
|
|
||||||
@ -69,15 +81,12 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
cd archive
|
cd archive
|
||||||
curl https://libre-tube.github.io/images/Alpha.png --output alpha.png
|
|
||||||
chmod 755 ./exec
|
|
||||||
mv ../tgconfig.py .
|
mv ../tgconfig.py .
|
||||||
echo "TG_TOKEN = '${{ secrets.TG_TOKEN }}'" >> tgconfig.py
|
echo "TG_TOKEN = '${{ secrets.TG_TOKEN }}'" >> tgconfig.py
|
||||||
echo "TG_API_ID = '${{ secrets.TG_API_ID }}'" >> tgconfig.py
|
echo "TG_API_ID = '${{ secrets.TG_API_ID }}'" >> tgconfig.py
|
||||||
echo "TG_POST_ID = '${{ secrets.TG_POST_ID }}'" >> tgconfig.py
|
echo "TG_POST_ID = '${{ secrets.TG_POST_ID }}'" >> tgconfig.py
|
||||||
echo "TG_API_HASH = '${{ secrets.TG_API_HASH }}'" >> tgconfig.py
|
echo "TG_API_HASH = '${{ secrets.TG_API_HASH }}'" >> tgconfig.py
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip TgCrypto Pyrogram
|
||||||
pip install python-telegram-bot
|
|
||||||
mv ../.github/tg.py .
|
mv ../.github/tg.py .
|
||||||
mv ../.github/commit.json .
|
mv ../.github/commit.json .
|
||||||
python tg.py
|
python tg.py
|
||||||
@ -86,4 +95,5 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: app
|
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" />
|
<img src="https://hosted.weblate.org/widgets/libretube/-/287x66-grey.png" alt="Translation status" />
|
||||||
</a>
|
</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.
|
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.
|
Feel free to help us if you have any knowledge concerning the following planned features or anything else you imagine.
|
||||||
|
|
||||||
## Planned
|
## Planned
|
||||||
- Support for local playlists
|
- Currently only various smaller features
|
||||||
- Various smaller features
|
|
||||||
|
|
||||||
## Not planned
|
## Not planned
|
||||||
- Google/MicroG Login
|
- Google/MicroG Login
|
||||||
|
@ -13,11 +13,17 @@ android {
|
|||||||
applicationId 'com.github.libretube'
|
applicationId 'com.github.libretube'
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 20
|
versionCode 23
|
||||||
versionName '0.6.1'
|
versionName '0.8.0'
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
resValue "string", "app_name", "LibreTube"
|
resValue "string", "app_name", "LibreTube"
|
||||||
|
|
||||||
|
javaCompileOptions {
|
||||||
|
annotationProcessorOptions {
|
||||||
|
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@ -41,6 +47,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
debuggable true
|
debuggable true
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
resValue "string", "app_name", "LibreTube Debug"
|
resValue "string", "app_name", "LibreTube Debug"
|
||||||
@ -101,6 +109,7 @@ dependencies {
|
|||||||
implementation libs.exoplayer
|
implementation libs.exoplayer
|
||||||
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
|
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
|
||||||
implementation libs.exoplayer.extension.mediasession
|
implementation libs.exoplayer.extension.mediasession
|
||||||
|
implementation libs.exoplayer.dash
|
||||||
|
|
||||||
/* Retrofit and Jackson */
|
/* Retrofit and Jackson */
|
||||||
implementation libs.square.retrofit
|
implementation libs.square.retrofit
|
||||||
|
13
app/proguard-rules.pro
vendored
13
app/proguard-rules.pro
vendored
@ -16,15 +16,16 @@
|
|||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# prevents obfuscation in debug logs
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
#uncomment for debug
|
#uncomment for debug
|
||||||
#-keepnames class **
|
#-keepnames class **
|
||||||
|
|
||||||
|
# Keep data classes used for Retrofit
|
||||||
-keep class com.github.libretube.obj.** { *; }
|
-keep class com.github.libretube.obj.** { *; }
|
||||||
|
-keep class com.github.libretube.obj.update.** { *; }
|
||||||
# prevents android from removing it
|
|
||||||
-keep class com.github.libretube.obj.**.** { *; }
|
|
||||||
|
|
||||||
# prevents obfuscation in debug logs
|
|
||||||
-dontobfuscate
|
|
||||||
|
@ -11,36 +11,10 @@
|
|||||||
"type": "UNIVERSAL",
|
"type": "UNIVERSAL",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 20,
|
"versionCode": 23,
|
||||||
"versionName": "0.6.1",
|
"versionName": "0.8.0",
|
||||||
"outputFile": "app-universal-release.apk"
|
"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",
|
"type": "ONE_OF_MANY",
|
||||||
"filters": [
|
"filters": [
|
||||||
@ -50,10 +24,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 20,
|
"versionCode": 23,
|
||||||
"versionName": "0.6.1",
|
"versionName": "0.8.0",
|
||||||
"outputFile": "app-x86-release.apk"
|
"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",
|
"type": "ONE_OF_MANY",
|
||||||
"filters": [
|
"filters": [
|
||||||
@ -63,9 +50,22 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 20,
|
"versionCode": 23,
|
||||||
"versionName": "0.6.1",
|
"versionName": "0.8.0",
|
||||||
"outputFile": "app-x86_64-release.apk"
|
"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"
|
"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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:installLocation="auto">
|
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@ -20,7 +27,8 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/StartupTheme"
|
android:theme="@style/StartupTheme"
|
||||||
tools:targetApi="n">
|
tools:targetApi="n"
|
||||||
|
android:banner="@mipmap/ic_launcher">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activities.NoInternetActivity"
|
android:name=".ui.activities.NoInternetActivity"
|
||||||
@ -59,6 +67,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -79,6 +90,32 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -99,6 +136,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -119,6 +159,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -137,6 +180,9 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -157,6 +203,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -177,6 +226,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
@ -197,6 +249,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@ -298,6 +353,7 @@
|
|||||||
|
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
<data android:host="piped.video" />
|
||||||
<data android:host="piped.tokhmi.xyz" />
|
<data android:host="piped.tokhmi.xyz" />
|
||||||
<data android:host="piped.kavin.rocks" />
|
<data android:host="piped.kavin.rocks" />
|
||||||
<data android:host="piped.silkky.cloud" />
|
<data android:host="piped.silkky.cloud" />
|
||||||
@ -329,4 +385,4 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
</application>
|
</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.ImageHelper
|
||||||
import com.github.libretube.util.NotificationHelper
|
import com.github.libretube.util.NotificationHelper
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
|
import com.github.libretube.util.ProxyHelper
|
||||||
|
|
||||||
class LibreTubeApp : Application() {
|
class LibreTubeApp : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -52,10 +53,16 @@ class LibreTubeApp : Application() {
|
|||||||
/**
|
/**
|
||||||
* Initialize the notification listener in the background
|
* Initialize the notification listener in the background
|
||||||
*/
|
*/
|
||||||
NotificationHelper(this).enqueueWork(
|
NotificationHelper.enqueueWork(
|
||||||
|
context = this,
|
||||||
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP
|
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the image proxy URL for local playlists and the watch history
|
||||||
|
*/
|
||||||
|
ProxyHelper.fetchProxyUrl()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for uncaught exceptions
|
* Handler for uncaught exceptions
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,22 @@
|
|||||||
package com.github.libretube.api
|
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.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
@ -8,81 +25,90 @@ import retrofit2.http.Path
|
|||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface PipedApi {
|
interface PipedApi {
|
||||||
|
@GET("config")
|
||||||
|
suspend fun getConfig(): PipedConfig
|
||||||
|
|
||||||
@GET("trending")
|
@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}")
|
@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}")
|
@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}")
|
@GET("sponsors/{videoId}")
|
||||||
suspend fun getSegments(
|
suspend fun getSegments(
|
||||||
@Path("videoId") videoId: String,
|
@Path("videoId") videoId: String,
|
||||||
@Query("category") category: String
|
@Query("category") category: String
|
||||||
): com.github.libretube.api.obj.Segments
|
): SegmentData
|
||||||
|
|
||||||
@GET("nextpage/comments/{videoId}")
|
@GET("nextpage/comments/{videoId}")
|
||||||
suspend fun getCommentsNextPage(
|
suspend fun getCommentsNextPage(
|
||||||
@Path("videoId") videoId: String,
|
@Path("videoId") videoId: String,
|
||||||
@Query("nextpage") nextPage: String
|
@Query("nextpage") nextPage: String
|
||||||
): com.github.libretube.api.obj.CommentsPage
|
): CommentsPage
|
||||||
|
|
||||||
@GET("search")
|
@GET("search")
|
||||||
suspend fun getSearchResults(
|
suspend fun getSearchResults(
|
||||||
@Query("q") searchQuery: String,
|
@Query("q") searchQuery: String,
|
||||||
@Query("filter") filter: String
|
@Query("filter") filter: String
|
||||||
): com.github.libretube.api.obj.SearchResult
|
): SearchResult
|
||||||
|
|
||||||
@GET("nextpage/search")
|
@GET("nextpage/search")
|
||||||
suspend fun getSearchResultsNextPage(
|
suspend fun getSearchResultsNextPage(
|
||||||
@Query("q") searchQuery: String,
|
@Query("q") searchQuery: String,
|
||||||
@Query("filter") filter: String,
|
@Query("filter") filter: String,
|
||||||
@Query("nextpage") nextPage: String
|
@Query("nextpage") nextPage: String
|
||||||
): com.github.libretube.api.obj.SearchResult
|
): SearchResult
|
||||||
|
|
||||||
@GET("suggestions")
|
@GET("suggestions")
|
||||||
suspend fun getSuggestions(@Query("query") query: String): List<String>
|
suspend fun getSuggestions(@Query("query") query: String): List<String>
|
||||||
|
|
||||||
@GET("channel/{channelId}")
|
@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}")
|
@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}")
|
@GET("nextpage/channel/{channelId}")
|
||||||
suspend fun getChannelNextPage(
|
suspend fun getChannelNextPage(
|
||||||
@Path("channelId") channelId: String,
|
@Path("channelId") channelId: String,
|
||||||
@Query("nextpage") nextPage: String
|
@Query("nextpage") nextPage: String
|
||||||
): com.github.libretube.api.obj.Channel
|
): Channel
|
||||||
|
|
||||||
@GET("playlists/{playlistId}")
|
@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}")
|
@GET("nextpage/playlists/{playlistId}")
|
||||||
suspend fun getPlaylistNextPage(
|
suspend fun getPlaylistNextPage(
|
||||||
@Path("playlistId") playlistId: String,
|
@Path("playlistId") playlistId: String,
|
||||||
@Query("nextpage") nextPage: String
|
@Query("nextpage") nextPage: String
|
||||||
): com.github.libretube.api.obj.Playlist
|
): Playlist
|
||||||
|
|
||||||
@POST("login")
|
@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")
|
@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")
|
@POST("user/delete")
|
||||||
suspend fun deleteAccount(
|
suspend fun deleteAccount(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body password: com.github.libretube.api.obj.DeleteUserRequest
|
@Body password: DeleteUserRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
@GET("feed")
|
@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")
|
@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")
|
@GET("subscribed")
|
||||||
suspend fun isSubscribed(
|
suspend fun isSubscribed(
|
||||||
@ -91,66 +117,66 @@ interface PipedApi {
|
|||||||
): com.github.libretube.api.obj.Subscribed
|
): com.github.libretube.api.obj.Subscribed
|
||||||
|
|
||||||
@GET("subscriptions")
|
@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")
|
@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")
|
@POST("subscribe")
|
||||||
suspend fun subscribe(
|
suspend fun subscribe(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body subscribe: com.github.libretube.api.obj.Subscribe
|
@Body subscribe: Subscribe
|
||||||
): com.github.libretube.api.obj.Message
|
): Message
|
||||||
|
|
||||||
@POST("unsubscribe")
|
@POST("unsubscribe")
|
||||||
suspend fun unsubscribe(
|
suspend fun unsubscribe(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body subscribe: com.github.libretube.api.obj.Subscribe
|
@Body subscribe: Subscribe
|
||||||
): com.github.libretube.api.obj.Message
|
): Message
|
||||||
|
|
||||||
@POST("import")
|
@POST("import")
|
||||||
suspend fun importSubscriptions(
|
suspend fun importSubscriptions(
|
||||||
@Query("override") override: Boolean,
|
@Query("override") override: Boolean,
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body channels: List<String>
|
@Body channels: List<String>
|
||||||
): com.github.libretube.api.obj.Message
|
): Message
|
||||||
|
|
||||||
@POST("import/playlist")
|
@POST("import/playlist")
|
||||||
suspend fun importPlaylist(
|
suspend fun importPlaylist(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body playlistId: com.github.libretube.api.obj.PlaylistId
|
@Body playlistId: PlaylistId
|
||||||
): com.github.libretube.api.obj.Message
|
): PlaylistId
|
||||||
|
|
||||||
@GET("user/playlists")
|
@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")
|
@POST("user/playlists/rename")
|
||||||
suspend fun renamePlaylist(
|
suspend fun renamePlaylist(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body playlistId: com.github.libretube.api.obj.PlaylistId
|
@Body playlistId: PlaylistId
|
||||||
)
|
)
|
||||||
|
|
||||||
@POST("user/playlists/delete")
|
@POST("user/playlists/delete")
|
||||||
suspend fun deletePlaylist(
|
suspend fun deletePlaylist(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body playlistId: com.github.libretube.api.obj.PlaylistId
|
@Body playlistId: PlaylistId
|
||||||
): com.github.libretube.api.obj.Message
|
): Message
|
||||||
|
|
||||||
@POST("user/playlists/create")
|
@POST("user/playlists/create")
|
||||||
suspend fun createPlaylist(
|
suspend fun createPlaylist(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body name: com.github.libretube.api.obj.Playlists
|
@Body name: Playlists
|
||||||
): com.github.libretube.api.obj.PlaylistId
|
): PlaylistId
|
||||||
|
|
||||||
@POST("user/playlists/add")
|
@POST("user/playlists/add")
|
||||||
suspend fun addToPlaylist(
|
suspend fun addToPlaylist(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body playlistId: com.github.libretube.api.obj.PlaylistId
|
@Body playlistId: PlaylistId
|
||||||
): com.github.libretube.api.obj.Message
|
): Message
|
||||||
|
|
||||||
@POST("user/playlists/remove")
|
@POST("user/playlists/remove")
|
||||||
suspend fun removeFromPlaylist(
|
suspend fun removeFromPlaylist(
|
||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body playlistId: com.github.libretube.api.obj.PlaylistId
|
@Body playlistId: PlaylistId
|
||||||
): com.github.libretube.api.obj.Message
|
): 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
|
package com.github.libretube.api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
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.DatabaseHolder.Companion.Database
|
||||||
import com.github.libretube.db.obj.LocalSubscription
|
import com.github.libretube.db.obj.LocalSubscription
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.extensions.awaitQuery
|
import com.github.libretube.extensions.awaitQuery
|
||||||
import com.github.libretube.extensions.query
|
import com.github.libretube.extensions.query
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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? {
|
suspend fun isSubscribed(channelId: String): Boolean? {
|
||||||
if (PreferenceHelper.getToken() != "") {
|
if (PreferenceHelper.getToken() != "") {
|
||||||
val isSubscribed = try {
|
val isSubscribed = try {
|
||||||
@ -99,7 +123,7 @@ object SubscriptionHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalSubscriptions(): List<LocalSubscription> {
|
private fun getLocalSubscriptions(): List<LocalSubscription> {
|
||||||
return awaitQuery {
|
return awaitQuery {
|
||||||
Database.localSubscriptionDao().getAll()
|
Database.localSubscriptionDao().getAll()
|
||||||
}
|
}
|
||||||
@ -107,6 +131,30 @@ object SubscriptionHelper {
|
|||||||
|
|
||||||
fun getFormattedLocalSubscriptions(): String {
|
fun getFormattedLocalSubscriptions(): String {
|
||||||
val localSubscriptions = getLocalSubscriptions()
|
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 nextpage: String? = null,
|
||||||
var subscriberCount: Long = 0,
|
var subscriberCount: Long = 0,
|
||||||
var verified: Boolean = false,
|
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
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Segments(
|
data class ChannelTab(
|
||||||
val segments: MutableList<Segment> = arrayListOf()
|
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(
|
data class CommentsPage(
|
||||||
val comments: MutableList<Comment> = arrayListOf(),
|
val comments: MutableList<Comment> = arrayListOf(),
|
||||||
val disabled: Boolean? = null,
|
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
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class SearchItem(
|
data class ContentItem(
|
||||||
var url: String? = null,
|
var url: String? = null,
|
||||||
var thumbnail: String? = null,
|
var thumbnail: String? = null,
|
||||||
var uploaderName: 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 indexEnd: Int? = null,
|
||||||
var width: Int? = null,
|
var width: Int? = null,
|
||||||
var height: 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 id: String? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var shortDescription: 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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val items: MutableList<SearchItem>? = arrayListOf(),
|
val items: MutableList<ContentItem>? = arrayListOf(),
|
||||||
val nextpage: String? = "",
|
val nextpage: String? = null,
|
||||||
val suggestion: String? = "",
|
val suggestion: String? = "",
|
||||||
val corrected: Boolean? = null
|
val corrected: Boolean? = null
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
|||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Segment(
|
data class Segment(
|
||||||
|
val UUID: String? = null,
|
||||||
val actionType: String? = null,
|
val actionType: String? = null,
|
||||||
val category: 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
|
* Links for the about fragment
|
||||||
*/
|
*/
|
||||||
const val WEBSITE_URL = "https://libre-tube.github.io/"
|
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 GITHUB_URL = "https://github.com/libre-tube/LibreTube"
|
||||||
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped"
|
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped"
|
||||||
const val WEBLATE_URL = "https://hosted.weblate.org/projects/libretube/libretube/"
|
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
|
* 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"
|
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 timeStamp = "timeStamp"
|
||||||
const val position = "position"
|
const val position = "position"
|
||||||
const val fileName = "fileName"
|
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 AUTH_PREF_FILE = "auth"
|
||||||
const val TOKEN = "token"
|
const val TOKEN = "token"
|
||||||
const val USERNAME = "username"
|
const val USERNAME = "username"
|
||||||
|
const val IMAGE_PROXY_URL = "image_proxy_url"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General
|
* General
|
||||||
@ -20,7 +21,7 @@ object PreferenceKeys {
|
|||||||
const val BREAK_REMINDER_TOGGLE = "break_reminder_toggle"
|
const val BREAK_REMINDER_TOGGLE = "break_reminder_toggle"
|
||||||
const val BREAK_REMINDER = "break_reminder"
|
const val BREAK_REMINDER = "break_reminder"
|
||||||
const val SAVE_FEED = "save_feed"
|
const val SAVE_FEED = "save_feed"
|
||||||
const val NAVBAR_ITEMS = "nav_bar_items"
|
const val NAVBAR_ITEMS = "navbar_items"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appearance
|
* Appearance
|
||||||
@ -33,7 +34,7 @@ object PreferenceKeys {
|
|||||||
const val APP_ICON = "icon_change"
|
const val APP_ICON = "icon_change"
|
||||||
const val LEGACY_SUBSCRIPTIONS = "legacy_subscriptions"
|
const val LEGACY_SUBSCRIPTIONS = "legacy_subscriptions"
|
||||||
const val LEGACY_SUBSCRIPTIONS_COLUMNS = "legacy_subscriptions_columns"
|
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 NEW_VIDEOS_BADGE = "new_videos_badge"
|
||||||
const val PLAYLISTS_ORDER = "playlists_order"
|
const val PLAYLISTS_ORDER = "playlists_order"
|
||||||
|
|
||||||
@ -77,13 +78,16 @@ object PreferenceKeys {
|
|||||||
const val PICTURE_IN_PICTURE = "picture_in_picture"
|
const val PICTURE_IN_PICTURE = "picture_in_picture"
|
||||||
const val PLAYER_RESIZE_MODE = "player_resize_mode"
|
const val PLAYER_RESIZE_MODE = "player_resize_mode"
|
||||||
const val SB_SKIP_MANUALLY = "sb_skip_manually_key"
|
const val SB_SKIP_MANUALLY = "sb_skip_manually_key"
|
||||||
const val LIMIT_HLS = "limit_hls"
|
const val SB_SHOW_MARKERS = "sb_show_markers"
|
||||||
const val PROGRESSIVE_LOADING_INTERVAL_SIZE = "progressive_loading_interval"
|
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
|
* Background mode
|
||||||
*/
|
*/
|
||||||
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
|
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
|
||||||
|
const val NOTIFICATION_OPEN_QUEUE = "notification_open_queue"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications
|
* Notifications
|
||||||
@ -92,6 +96,10 @@ object PreferenceKeys {
|
|||||||
const val CHECKING_FREQUENCY = "checking_frequency"
|
const val CHECKING_FREQUENCY = "checking_frequency"
|
||||||
const val REQUIRED_NETWORK = "required_network"
|
const val REQUIRED_NETWORK = "required_network"
|
||||||
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
|
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
|
* Advanced
|
||||||
@ -103,6 +111,8 @@ object PreferenceKeys {
|
|||||||
const val CLEAR_WATCH_HISTORY = "clear_watch_history"
|
const val CLEAR_WATCH_HISTORY = "clear_watch_history"
|
||||||
const val CLEAR_WATCH_POSITIONS = "clear_watch_positions"
|
const val CLEAR_WATCH_POSITIONS = "clear_watch_positions"
|
||||||
const val SHARE_WITH_TIME_CODE = "share_with_time_code"
|
const val SHARE_WITH_TIME_CODE = "share_with_time_code"
|
||||||
|
const val CONFIRM_UNSUBSCRIBE = "confirm_unsubscribing"
|
||||||
|
const val CLEAR_BOOKMARKS = "clear_bookmarks"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* History
|
* 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
|
package com.github.libretube.db
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import com.github.libretube.db.dao.CustomInstanceDao
|
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.LocalSubscriptionDao
|
||||||
|
import com.github.libretube.db.dao.PlaylistBookmarkDao
|
||||||
import com.github.libretube.db.dao.SearchHistoryDao
|
import com.github.libretube.db.dao.SearchHistoryDao
|
||||||
import com.github.libretube.db.dao.WatchHistoryDao
|
import com.github.libretube.db.dao.WatchHistoryDao
|
||||||
import com.github.libretube.db.dao.WatchPositionDao
|
import com.github.libretube.db.dao.WatchPositionDao
|
||||||
import com.github.libretube.db.obj.CustomInstance
|
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.LocalSubscription
|
||||||
|
import com.github.libretube.db.obj.PlaylistBookmark
|
||||||
import com.github.libretube.db.obj.SearchHistoryItem
|
import com.github.libretube.db.obj.SearchHistoryItem
|
||||||
import com.github.libretube.db.obj.WatchHistoryItem
|
import com.github.libretube.db.obj.WatchHistoryItem
|
||||||
import com.github.libretube.db.obj.WatchPosition
|
import com.github.libretube.db.obj.WatchPosition
|
||||||
@ -19,9 +25,16 @@ import com.github.libretube.db.obj.WatchPosition
|
|||||||
WatchPosition::class,
|
WatchPosition::class,
|
||||||
SearchHistoryItem::class,
|
SearchHistoryItem::class,
|
||||||
CustomInstance::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() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
/**
|
/**
|
||||||
@ -48,4 +61,14 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
* Local Subscriptions
|
* Local Subscriptions
|
||||||
*/
|
*/
|
||||||
abstract fun localSubscriptionDao(): LocalSubscriptionDao
|
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.DatabaseHolder.Companion.Database
|
||||||
import com.github.libretube.db.obj.SearchHistoryItem
|
import com.github.libretube.db.obj.SearchHistoryItem
|
||||||
import com.github.libretube.db.obj.WatchHistoryItem
|
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.query
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
|
|
||||||
object DatabaseHelper {
|
object DatabaseHelper {
|
||||||
|
private const val MAX_SEARCH_HISTORY_SIZE = 20
|
||||||
|
|
||||||
fun addToWatchHistory(videoId: String, streams: Streams) {
|
fun addToWatchHistory(videoId: String, streams: Streams) {
|
||||||
val watchHistoryItem = WatchHistoryItem(
|
val watchHistoryItem = WatchHistoryItem(
|
||||||
videoId,
|
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) {
|
fun addToSearchHistory(searchHistoryItem: SearchHistoryItem) {
|
||||||
query {
|
query {
|
||||||
Database.searchHistoryDao().insertAll(searchHistoryItem)
|
Database.searchHistoryDao().insertAll(searchHistoryItem)
|
||||||
val maxHistorySize = 20
|
|
||||||
|
|
||||||
// delete the first watch history entry if the limit is reached
|
// delete the first watch history entry if the limit is reached
|
||||||
val searchHistory = Database.searchHistoryDao().getAll()
|
val searchHistory = Database.searchHistoryDao().getAll()
|
||||||
if (searchHistory.size > maxHistorySize) {
|
if (searchHistory.size > MAX_SEARCH_HISTORY_SIZE) {
|
||||||
Database.searchHistoryDao()
|
Database.searchHistoryDao()
|
||||||
.delete(searchHistory.first())
|
.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
|
@Delete
|
||||||
fun delete(watchPosition: WatchPosition)
|
fun delete(watchPosition: WatchPosition)
|
||||||
|
|
||||||
|
@Query("DELETE FROM watchHistoryItem WHERE videoId = :videoId")
|
||||||
|
fun deleteById(videoId: String)
|
||||||
|
|
||||||
@Query("DELETE FROM watchPosition")
|
@Query("DELETE FROM watchPosition")
|
||||||
fun deleteAll()
|
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 uploadDate: String? = null,
|
||||||
@ColumnInfo val uploader: String? = null,
|
@ColumnInfo val uploader: String? = null,
|
||||||
@ColumnInfo val uploaderUrl: String? = null,
|
@ColumnInfo val uploaderUrl: String? = null,
|
||||||
@ColumnInfo val uploaderAvatar: String? = null,
|
@ColumnInfo var uploaderAvatar: String? = null,
|
||||||
@ColumnInfo val thumbnailUrl: String? = null,
|
@ColumnInfo var thumbnailUrl: String? = null,
|
||||||
@ColumnInfo val duration: Long? = 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.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
|
||||||
|
@Suppress("KotlinConstantConditions")
|
||||||
fun Long?.formatShort(): String = when {
|
fun Long?.formatShort(): String = when {
|
||||||
this!! < 1000 -> {
|
this == null -> (0).toString()
|
||||||
|
this < 1000 -> {
|
||||||
this.toString()
|
this.toString()
|
||||||
}
|
}
|
||||||
this in 1000..999999 -> {
|
this in (1000..999999) -> {
|
||||||
val decimal = BigDecimal(this / 1000).setScale(0, RoundingMode.HALF_EVEN)
|
val decimal = BigDecimal(this / 1000).setScale(0, RoundingMode.HALF_EVEN)
|
||||||
decimal.toString() + "K"
|
decimal.toString() + "K"
|
||||||
}
|
}
|
||||||
this in 1000000..10000000 -> {
|
this in (1000000..10000000) -> {
|
||||||
val decimal = BigDecimal(this / 1000000).setScale(0, RoundingMode.HALF_EVEN)
|
val decimal = BigDecimal(this / 1000000).setScale(0, RoundingMode.HALF_EVEN)
|
||||||
decimal.toString() + "M"
|
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
|
* format a Piped route to an ID
|
||||||
*/
|
*/
|
||||||
fun Any.toID(): String {
|
fun String.toID(): String {
|
||||||
return this
|
return this
|
||||||
.toString()
|
|
||||||
.replace("/watch?v=", "") // videos
|
.replace("/watch?v=", "") // videos
|
||||||
.replace("/channel/", "") // channels
|
.replace("/channel/", "") // channels
|
||||||
.replace("/playlist?list=", "") // playlists
|
.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
|
package com.github.libretube.obj
|
||||||
|
|
||||||
import com.github.libretube.db.obj.CustomInstance
|
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.LocalSubscription
|
||||||
|
import com.github.libretube.db.obj.PlaylistBookmark
|
||||||
import com.github.libretube.db.obj.SearchHistoryItem
|
import com.github.libretube.db.obj.SearchHistoryItem
|
||||||
import com.github.libretube.db.obj.WatchHistoryItem
|
import com.github.libretube.db.obj.WatchHistoryItem
|
||||||
import com.github.libretube.db.obj.WatchPosition
|
import com.github.libretube.db.obj.WatchPosition
|
||||||
@ -12,5 +14,7 @@ data class BackupFile(
|
|||||||
var searchHistory: List<SearchHistoryItem>? = null,
|
var searchHistory: List<SearchHistoryItem>? = null,
|
||||||
var localSubscriptions: List<LocalSubscription>? = null,
|
var localSubscriptions: List<LocalSubscription>? = null,
|
||||||
var customInstances: List<CustomInstance>? = null,
|
var customInstances: List<CustomInstance>? = null,
|
||||||
|
var playlistBookmarks: List<PlaylistBookmark>? = null,
|
||||||
|
var localPlaylists: List<LocalPlaylistWithVideos>? = null,
|
||||||
var preferences: List<PreferenceItem>? = null
|
var preferences: List<PreferenceItem>? = null
|
||||||
)
|
)
|
||||||
|
@ -3,5 +3,6 @@ package com.github.libretube.obj
|
|||||||
data class BottomSheetItem(
|
data class BottomSheetItem(
|
||||||
val title: String,
|
val title: String,
|
||||||
val drawable: Int? = null,
|
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(
|
data class DownloadedFile(
|
||||||
val name: String,
|
val name: String,
|
||||||
val size: Long,
|
val size: Long,
|
||||||
val type: Int,
|
|
||||||
var metadata: Streams? = null,
|
var metadata: Streams? = null,
|
||||||
var thumbnail: Bitmap? = 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.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.obj.Segment
|
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.api.obj.Streams
|
||||||
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
|
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
|
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.db.DatabaseHelper
|
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
||||||
import com.github.libretube.db.DatabaseHolder
|
import com.github.libretube.db.obj.WatchPosition
|
||||||
|
import com.github.libretube.enums.PlaylistType
|
||||||
import com.github.libretube.extensions.awaitQuery
|
import com.github.libretube.extensions.awaitQuery
|
||||||
|
import com.github.libretube.extensions.query
|
||||||
import com.github.libretube.extensions.toID
|
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.NowPlayingNotification
|
||||||
import com.github.libretube.util.PlayerHelper
|
import com.github.libretube.util.PlayerHelper
|
||||||
import com.github.libretube.util.PlayingQueue
|
import com.github.libretube.util.PlayingQueue
|
||||||
@ -53,6 +54,7 @@ class BackgroundMode : Service() {
|
|||||||
*PlaylistId for autoplay
|
*PlaylistId for autoplay
|
||||||
*/
|
*/
|
||||||
private var playlistId: String? = null
|
private var playlistId: String? = null
|
||||||
|
private var playlistType: PlaylistType? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response that gets when called the Api.
|
* The response that gets when called the Api.
|
||||||
@ -73,23 +75,13 @@ class BackgroundMode : Service() {
|
|||||||
/**
|
/**
|
||||||
* SponsorBlock Segment data
|
* SponsorBlock Segment data
|
||||||
*/
|
*/
|
||||||
private var segmentData: Segments? = null
|
private var segmentData: SegmentData? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Notification] for the player
|
* [Notification] for the player
|
||||||
*/
|
*/
|
||||||
private lateinit var nowPlayingNotification: NowPlayingNotification
|
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
|
* Autoplay Preference
|
||||||
*/
|
*/
|
||||||
@ -100,18 +92,21 @@ class BackgroundMode : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channelId = BACKGROUND_CHANNEL_ID
|
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
channelId,
|
BACKGROUND_CHANNEL_ID,
|
||||||
"Background Service",
|
"Background Service",
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
)
|
)
|
||||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.createNotificationChannel(channel)
|
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))
|
.setContentTitle(getString(R.string.app_name))
|
||||||
.setContentText(getString(R.string.playingOnBackground)).build()
|
.setContentText(getString(R.string.playingOnBackground))
|
||||||
|
.build()
|
||||||
|
|
||||||
startForeground(PLAYER_NOTIFICATION_ID, notification)
|
startForeground(PLAYER_NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,20 +117,21 @@ class BackgroundMode : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
try {
|
try {
|
||||||
// clear the playing queue
|
// clear the playing queue
|
||||||
PlayingQueue.clear()
|
PlayingQueue.resetToDefaults()
|
||||||
|
|
||||||
// get the intent arguments
|
// get the intent arguments
|
||||||
videoId = intent?.getStringExtra(IntentData.videoId)!!
|
videoId = intent?.getStringExtra(IntentData.videoId)!!
|
||||||
playlistId = intent.getStringExtra(IntentData.playlistId)
|
playlistId = intent.getStringExtra(IntentData.playlistId)
|
||||||
val position = intent.getLongExtra(IntentData.position, 0L)
|
val position = intent.getLongExtra(IntentData.position, 0L)
|
||||||
|
|
||||||
// initialize the playlist autoPlay Helper
|
|
||||||
autoPlayHelper = AutoPlayHelper(playlistId)
|
|
||||||
|
|
||||||
// play the audio in the background
|
// play the audio in the background
|
||||||
loadAudio(videoId, position)
|
loadAudio(videoId, position)
|
||||||
|
|
||||||
updateWatchPosition()
|
PlayingQueue.setOnQueueTapListener { streamItem ->
|
||||||
|
streamItem.url?.toID()?.let { playNextVideo(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlayerHelper.watchPositionsEnabled) updateWatchPosition()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onDestroy()
|
onDestroy()
|
||||||
}
|
}
|
||||||
@ -143,7 +139,11 @@ class BackgroundMode : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWatchPosition() {
|
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)
|
handler.postDelayed(this::updateWatchPosition, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,8 +154,6 @@ class BackgroundMode : Service() {
|
|||||||
videoId: String,
|
videoId: String,
|
||||||
seekToPosition: Long = 0
|
seekToPosition: Long = 0
|
||||||
) {
|
) {
|
||||||
// append the video to the playing queue
|
|
||||||
PlayingQueue.add(videoId)
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
streams = RetrofitInstance.api.getStreams(videoId)
|
streams = RetrofitInstance.api.getStreams(videoId)
|
||||||
@ -163,6 +161,21 @@ class BackgroundMode : Service() {
|
|||||||
return@launch
|
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 {
|
handler.post {
|
||||||
playAudio(seekToPosition)
|
playAudio(seekToPosition)
|
||||||
}
|
}
|
||||||
@ -172,8 +185,6 @@ class BackgroundMode : Service() {
|
|||||||
private fun playAudio(
|
private fun playAudio(
|
||||||
seekToPosition: Long
|
seekToPosition: Long
|
||||||
) {
|
) {
|
||||||
PlayingQueue.updateCurrent(videoId)
|
|
||||||
|
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
setMediaItem()
|
setMediaItem()
|
||||||
|
|
||||||
@ -194,9 +205,8 @@ class BackgroundMode : Service() {
|
|||||||
} else if (PlayerHelper.watchPositionsEnabled) {
|
} else if (PlayerHelper.watchPositionsEnabled) {
|
||||||
try {
|
try {
|
||||||
val watchPosition = awaitQuery {
|
val watchPosition = awaitQuery {
|
||||||
DatabaseHolder.Database.watchPositionDao().findById(videoId)
|
Database.watchPositionDao().findById(videoId)
|
||||||
}
|
}
|
||||||
Log.e("position", watchPosition.toString())
|
|
||||||
streams?.duration?.let {
|
streams?.duration?.let {
|
||||||
if (watchPosition != null && watchPosition.position < it * 1000 * 0.9) {
|
if (watchPosition != null && watchPosition.position < it * 1000 * 0.9) {
|
||||||
player?.seekTo(watchPosition.position)
|
player?.seekTo(watchPosition.position)
|
||||||
@ -215,8 +225,6 @@ class BackgroundMode : Service() {
|
|||||||
player?.setPlaybackSpeed(playbackSpeed)
|
player?.setPlaybackSpeed(playbackSpeed)
|
||||||
|
|
||||||
fetchSponsorBlockSegments()
|
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)
|
* Plays the first related video to the current (used when the playback of the current video ended)
|
||||||
*/
|
*/
|
||||||
private fun playNextVideo() {
|
private fun playNextVideo(nextId: String? = null) {
|
||||||
if (nextStreamId == null || nextStreamId == videoId) return
|
val nextVideo = nextId ?: PlayingQueue.getNext()
|
||||||
val nextQueueVideo = PlayingQueue.getNext()
|
|
||||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
|
||||||
|
|
||||||
// play new video on background
|
// play new video on background
|
||||||
this.videoId = nextStreamId!!
|
if (nextVideo != null) {
|
||||||
|
this.videoId = nextVideo
|
||||||
|
}
|
||||||
this.segmentData = null
|
this.segmentData = null
|
||||||
loadAudio(videoId)
|
loadAudio(videoId)
|
||||||
}
|
}
|
||||||
@ -322,16 +314,15 @@ class BackgroundMode : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun fetchSponsorBlockSegments() {
|
private fun fetchSponsorBlockSegments() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
kotlin.runCatching {
|
runCatching {
|
||||||
val categories = PlayerHelper.getSponsorBlockCategories()
|
val categories = PlayerHelper.getSponsorBlockCategories()
|
||||||
if (categories.size > 0) {
|
if (categories.isEmpty()) return@runCatching
|
||||||
segmentData =
|
segmentData =
|
||||||
RetrofitInstance.api.getSegments(
|
RetrofitInstance.api.getSegments(
|
||||||
videoId,
|
videoId,
|
||||||
ObjectMapper().writeValueAsString(categories)
|
ObjectMapper().writeValueAsString(categories)
|
||||||
)
|
)
|
||||||
checkForSegments()
|
checkForSegments()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,7 +336,7 @@ class BackgroundMode : Service() {
|
|||||||
if (segmentData == null || segmentData!!.segments.isEmpty()) return
|
if (segmentData == null || segmentData!!.segments.isEmpty()) return
|
||||||
|
|
||||||
segmentData!!.segments.forEach { segment: Segment ->
|
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 segmentEnd = (segment.segment[1] * 1000f).toLong()
|
||||||
val currentPosition = player?.currentPosition
|
val currentPosition = player?.currentPosition
|
||||||
if (currentPosition in segmentStart until segmentEnd) {
|
if (currentPosition in segmentStart until segmentEnd) {
|
||||||
@ -367,7 +358,7 @@ class BackgroundMode : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
// clear the playing queue
|
// clear the playing queue
|
||||||
PlayingQueue.clear()
|
PlayingQueue.resetToDefaults()
|
||||||
|
|
||||||
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroySelfAndPlayer()
|
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_CHANNEL_ID
|
||||||
import com.github.libretube.constants.DOWNLOAD_FAILURE_NOTIFICATION_ID
|
import com.github.libretube.constants.DOWNLOAD_FAILURE_NOTIFICATION_ID
|
||||||
import com.github.libretube.constants.DOWNLOAD_SUCCESS_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.extensions.TAG
|
||||||
import com.github.libretube.util.DownloadHelper
|
import com.github.libretube.util.DownloadHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -25,7 +25,7 @@ class DownloadService : Service() {
|
|||||||
private lateinit var videoName: String
|
private lateinit var videoName: String
|
||||||
private lateinit var videoUrl: String
|
private lateinit var videoUrl: String
|
||||||
private lateinit var audioUrl: String
|
private lateinit var audioUrl: String
|
||||||
private var downloadType: Int = 3
|
private var downloadType: DownloadType = DownloadType.NONE
|
||||||
|
|
||||||
private var videoDownloadId: Long? = null
|
private var videoDownloadId: Long? = null
|
||||||
private var audioDownloadId: Long? = null
|
private var audioDownloadId: Long? = null
|
||||||
@ -63,8 +63,8 @@ class DownloadService : Service() {
|
|||||||
private fun downloadManager() {
|
private fun downloadManager() {
|
||||||
// initialize and create the directories to download into
|
// initialize and create the directories to download into
|
||||||
|
|
||||||
val videoDownloadDir = DownloadHelper.getVideoDir(this)
|
val videoDownloadDir = DownloadHelper.getDownloadDir(this, DownloadHelper.VIDEO_DIR)
|
||||||
val audioDownloadDir = DownloadHelper.getAudioDir(this)
|
val audioDownloadDir = DownloadHelper.getDownloadDir(this, DownloadHelper.AUDIO_DIR)
|
||||||
|
|
||||||
// start download
|
// start download
|
||||||
try {
|
try {
|
||||||
@ -74,7 +74,7 @@ class DownloadService : Service() {
|
|||||||
)
|
)
|
||||||
if (downloadType in listOf(DownloadType.VIDEO, DownloadType.AUDIO_VIDEO)) {
|
if (downloadType in listOf(DownloadType.VIDEO, DownloadType.AUDIO_VIDEO)) {
|
||||||
videoDownloadId = downloadManagerRequest(
|
videoDownloadId = downloadManagerRequest(
|
||||||
getString(R.string.video),
|
"[${getString(R.string.video)}] $videoName",
|
||||||
getString(R.string.downloading),
|
getString(R.string.downloading),
|
||||||
videoUrl,
|
videoUrl,
|
||||||
Uri.fromFile(
|
Uri.fromFile(
|
||||||
@ -84,7 +84,7 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
if (downloadType in listOf(DownloadType.AUDIO, DownloadType.AUDIO_VIDEO)) {
|
if (downloadType in listOf(DownloadType.AUDIO, DownloadType.AUDIO_VIDEO)) {
|
||||||
audioDownloadId = downloadManagerRequest(
|
audioDownloadId = downloadManagerRequest(
|
||||||
getString(R.string.audio),
|
"[${getString(R.string.audio)}] $videoName",
|
||||||
getString(R.string.downloading),
|
getString(R.string.downloading),
|
||||||
audioUrl,
|
audioUrl,
|
||||||
Uri.fromFile(
|
Uri.fromFile(
|
||||||
|
@ -11,6 +11,7 @@ import android.os.Environment
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.util.DownloadHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class UpdateService : Service() {
|
class UpdateService : Service() {
|
||||||
@ -28,9 +29,7 @@ class UpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadApk(downloadUrl: String) {
|
private fun downloadApk(downloadUrl: String) {
|
||||||
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
file = File(getDownloadDirectory(), "release.apk")
|
||||||
// val dir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
file = File(dir, "release.apk")
|
|
||||||
|
|
||||||
val request: DownloadManager.Request =
|
val request: DownloadManager.Request =
|
||||||
DownloadManager.Request(Uri.parse(downloadUrl))
|
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() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(onDownloadComplete)
|
unregisterReceiver(onDownloadComplete)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
@ -14,10 +14,10 @@ import android.view.View
|
|||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
@ -27,18 +27,20 @@ import com.github.libretube.constants.IntentData
|
|||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.databinding.ActivityMainBinding
|
import com.github.libretube.databinding.ActivityMainBinding
|
||||||
import com.github.libretube.extensions.toID
|
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.services.ClosingService
|
||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
import com.github.libretube.ui.dialogs.ErrorDialog
|
import com.github.libretube.ui.dialogs.ErrorDialog
|
||||||
import com.github.libretube.ui.fragments.PlayerFragment
|
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.NavBarHelper
|
||||||
import com.github.libretube.util.NetworkHelper
|
import com.github.libretube.util.NetworkHelper
|
||||||
|
import com.github.libretube.util.PlayingQueue
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
import com.github.libretube.util.ThemeHelper
|
import com.github.libretube.util.ThemeHelper
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.elevation.SurfaceColors
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
|
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
@ -47,21 +49,17 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
lateinit var navController: NavController
|
lateinit var navController: NavController
|
||||||
private var startFragmentId = R.id.homeFragment
|
private var startFragmentId = R.id.homeFragment
|
||||||
var autoRotationEnabled = false
|
|
||||||
|
val autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
|
||||||
|
|
||||||
lateinit var searchView: SearchView
|
lateinit var searchView: SearchView
|
||||||
|
lateinit var searchItem: MenuItem
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
|
|
||||||
|
|
||||||
// enable auto rotation if turned on
|
// enable auto rotation if turned on
|
||||||
requestedOrientation = if (autoRotationEnabled) {
|
requestOrientationChange()
|
||||||
ActivityInfo.SCREEN_ORIENTATION_USER
|
|
||||||
} else {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
|
||||||
}
|
|
||||||
|
|
||||||
// start service that gets called on closure
|
// start service that gets called on closure
|
||||||
try {
|
try {
|
||||||
@ -93,8 +91,12 @@ class MainActivity : BaseActivity() {
|
|||||||
// sets the navigation bar color to the previously calculated color
|
// sets the navigation bar color to the previously calculated color
|
||||||
window.navigationBarColor = color
|
window.navigationBarColor = color
|
||||||
|
|
||||||
// save start tab fragment id
|
// save start tab fragment id and apply navbar style
|
||||||
startFragmentId = NavBarHelper.applyNavBarStyle(binding.bottomNav)
|
startFragmentId = try {
|
||||||
|
NavBarHelper.applyNavBarStyle(binding.bottomNav)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
R.id.homeFragment
|
||||||
|
}
|
||||||
|
|
||||||
// set default tab as start fragment
|
// set default tab as start fragment
|
||||||
navController.graph.setStartDestination(startFragmentId)
|
navController.graph.setStartDestination(startFragmentId)
|
||||||
@ -104,18 +106,16 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
binding.bottomNav.setOnApplyWindowInsetsListener(null)
|
binding.bottomNav.setOnApplyWindowInsetsListener(null)
|
||||||
|
|
||||||
binding.bottomNav.setOnItemSelectedListener {
|
// Prevent duplicate entries into backstack, if selected item and current
|
||||||
// clear backstack if it's the start fragment
|
// visible fragment is different, then navigate to selected item.
|
||||||
if (startFragmentId == it.itemId) navController.backQueue.clear()
|
binding.bottomNav.setOnItemReselectedListener {
|
||||||
|
if (it.itemId != navController.currentDestination?.id) {
|
||||||
if (it.itemId == R.id.subscriptionsFragment) {
|
navigateToBottomSelectedItem(it)
|
||||||
binding.bottomNav.removeBadge(R.id.subscriptionsFragment)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removeSearchFocus()
|
binding.bottomNav.setOnItemSelectedListener {
|
||||||
|
navigateToBottomSelectedItem(it)
|
||||||
// navigate to the selected fragment
|
|
||||||
navController.navigate(it.itemId)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ class MainActivity : BaseActivity() {
|
|||||||
val log = PreferenceHelper.getErrorLog()
|
val log = PreferenceHelper.getErrorLog()
|
||||||
if (log != "") ErrorDialog().show(supportFragmentManager, null)
|
if (log != "") ErrorDialog().show(supportFragmentManager, null)
|
||||||
|
|
||||||
setupBreakReminder()
|
BreakReminder.setupBreakReminder(applicationContext)
|
||||||
|
|
||||||
setupSubscriptionsBadge()
|
setupSubscriptionsBadge()
|
||||||
|
|
||||||
@ -143,56 +143,38 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navController.currentDestination?.id == startFragmentId) {
|
when (navController.currentDestination?.id) {
|
||||||
moveTaskToBack(true)
|
startFragmentId -> {
|
||||||
} else {
|
moveTaskToBack(true)
|
||||||
navController.popBackStack()
|
}
|
||||||
|
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() {
|
fun requestOrientationChange() {
|
||||||
if (!PreferenceHelper.getBoolean(
|
requestedOrientation = if (autoRotationEnabled) {
|
||||||
PreferenceKeys.BREAK_REMINDER_TOGGLE,
|
ActivityInfo.SCREEN_ORIENTATION_USER
|
||||||
false
|
} else {
|
||||||
)
|
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
val breakReminderPref = PreferenceHelper.getString(
|
}
|
||||||
PreferenceKeys.BREAK_REMINDER,
|
|
||||||
"0"
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
)
|
menu?.findItem(R.id.action_queue)?.isVisible = PlayingQueue.isNotEmpty()
|
||||||
if (!breakReminderPref.all { Character.isDigit(it) } ||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,6 +209,8 @@ class MainActivity : BaseActivity() {
|
|||||||
private fun removeSearchFocus() {
|
private fun removeSearchFocus() {
|
||||||
searchView.setQuery("", false)
|
searchView.setQuery("", false)
|
||||||
searchView.clearFocus()
|
searchView.clearFocus()
|
||||||
|
searchView.isIconified = true
|
||||||
|
searchItem.collapseActionView()
|
||||||
searchView.onActionViewCollapsed()
|
searchView.onActionViewCollapsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,27 +220,31 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
// stuff for the search in the topBar
|
// stuff for the search in the topBar
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
|
this.searchItem = searchItem
|
||||||
searchView = searchItem.actionView as SearchView
|
searchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
val searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
|
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 {
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString("query", query)
|
bundle.putString("query", query)
|
||||||
navController.navigate(R.id.searchResultFragment, bundle)
|
navController.navigate(R.id.searchResultFragment, bundle)
|
||||||
searchViewModel.setQuery("")
|
searchViewModel.setQuery("")
|
||||||
|
searchView.clearFocus()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
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
|
// prevent malicious navigation when the search view is getting collapsed
|
||||||
if (navController.currentDestination?.id in listOf(
|
if (navController.currentDestination?.id in listOf(
|
||||||
R.id.searchResultFragment,
|
R.id.searchResultFragment,
|
||||||
@ -279,6 +267,36 @@ class MainActivity : BaseActivity() {
|
|||||||
return true
|
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)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,16 +320,14 @@ class MainActivity : BaseActivity() {
|
|||||||
startActivity(communityIntent)
|
startActivity(communityIntent)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_queue -> {
|
||||||
|
PlayingQueueSheet().show(supportFragmentManager, null)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
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() {
|
private fun loadIntentData() {
|
||||||
intent?.getStringExtra(IntentData.channelId)?.let {
|
intent?.getStringExtra(IntentData.channelId)?.let {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@ -334,6 +350,20 @@ class MainActivity : BaseActivity() {
|
|||||||
intent?.getStringExtra(IntentData.videoId)?.let {
|
intent?.getStringExtra(IntentData.videoId)?.let {
|
||||||
loadVideo(it, intent?.getLongExtra(IntentData.timeStamp, 0L))
|
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?) {
|
private fun loadVideo(videoId: String, timeStamp: Long?) {
|
||||||
@ -359,7 +389,7 @@ class MainActivity : BaseActivity() {
|
|||||||
transitionToStart()
|
transitionToStart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun minimizePlayer() {
|
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() {
|
override fun onUserLeaveHint() {
|
||||||
super.onUserLeaveHint()
|
super.onUserLeaveHint()
|
||||||
supportFragmentManager.fragments.forEach { fragment ->
|
supportFragmentManager.fragments.forEach { fragment ->
|
||||||
|
@ -68,28 +68,26 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.player.initialize(
|
binding.player.initialize(
|
||||||
supportFragmentManager,
|
|
||||||
null,
|
null,
|
||||||
binding.doubleTapOverlay.binding,
|
binding.doubleTapOverlay.binding,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun File.toUri(): Uri? {
|
||||||
|
return if (this.exists()) Uri.fromFile(this) else null
|
||||||
|
}
|
||||||
|
|
||||||
private fun playVideo() {
|
private fun playVideo() {
|
||||||
val videoDownloadDir = DownloadHelper.getVideoDir(this)
|
val videoUri = File(
|
||||||
val videoFile = File(
|
DownloadHelper.getDownloadDir(this, DownloadHelper.VIDEO_DIR),
|
||||||
videoDownloadDir,
|
|
||||||
fileName
|
fileName
|
||||||
)
|
).toUri()
|
||||||
|
|
||||||
val audioDownloadDir = DownloadHelper.getAudioDir(this)
|
val audioUri = File(
|
||||||
val audioFile = File(
|
DownloadHelper.getDownloadDir(this, DownloadHelper.AUDIO_DIR),
|
||||||
audioDownloadDir,
|
|
||||||
fileName
|
fileName
|
||||||
)
|
).toUri()
|
||||||
|
|
||||||
val videoUri = if (videoFile.exists()) Uri.fromFile(videoFile) else null
|
|
||||||
val audioUri = if (audioFile.exists()) Uri.fromFile(audioFile) else null
|
|
||||||
|
|
||||||
setMediaSource(
|
setMediaSource(
|
||||||
videoUri,
|
videoUri,
|
||||||
|
@ -10,6 +10,7 @@ import com.github.libretube.constants.IntentData
|
|||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class RouterActivity : BaseActivity() {
|
class RouterActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -75,14 +76,14 @@ class RouterActivity : BaseActivity() {
|
|||||||
|
|
||||||
intent.putExtra(IntentData.videoId, videoId)
|
intent.putExtra(IntentData.videoId, videoId)
|
||||||
uri.getQueryParameter("t")
|
uri.getQueryParameter("t")
|
||||||
?.let { intent.putExtra(IntentData.timeStamp, it.toLong()) }
|
?.let { intent.putExtra(IntentData.timeStamp, parseTimestamp(it)) }
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val videoId = uri.path!!.replace("/", "")
|
val videoId = uri.path!!.replace("/", "")
|
||||||
|
|
||||||
intent.putExtra(IntentData.videoId, videoId)
|
intent.putExtra(IntentData.videoId, videoId)
|
||||||
uri.getQueryParameter("t")
|
uri.getQueryParameter("t")
|
||||||
?.let { intent.putExtra(IntentData.timeStamp, it.toLong()) }
|
?.let { intent.putExtra(IntentData.timeStamp, parseTimestamp(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
@ -99,4 +100,12 @@ class RouterActivity : BaseActivity() {
|
|||||||
)
|
)
|
||||||
this.finishAndRemoveTask()
|
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) {
|
override fun onBindViewHolder(holder: BottomSheetViewHolder, position: Int) {
|
||||||
val item = items[position]
|
val item = items[position]
|
||||||
holder.binding.apply {
|
holder.binding.apply {
|
||||||
|
val current = item.getCurrent()
|
||||||
title.text =
|
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) {
|
if (item.drawable != null) {
|
||||||
drawable.setImageResource(item.drawable)
|
drawable.setImageResource(item.drawable)
|
||||||
} else {
|
} else {
|
||||||
drawable.visibility =
|
drawable.visibility = View.GONE
|
||||||
View.GONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
|
item.onClick.invoke()
|
||||||
listener.invoke(position)
|
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
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.text.format.DateUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
import com.github.libretube.databinding.ChapterColumnBinding
|
import com.github.libretube.databinding.ChapterColumnBinding
|
||||||
import com.github.libretube.ui.viewholders.ChaptersViewHolder
|
import com.github.libretube.ui.viewholders.ChaptersViewHolder
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
@ -11,7 +13,7 @@ import com.github.libretube.util.ThemeHelper
|
|||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
|
|
||||||
class ChaptersAdapter(
|
class ChaptersAdapter(
|
||||||
private val chapters: List<com.github.libretube.api.obj.ChapterSegment>,
|
private val chapters: List<ChapterSegment>,
|
||||||
private val exoPlayer: ExoPlayer
|
private val exoPlayer: ExoPlayer
|
||||||
) : RecyclerView.Adapter<ChaptersViewHolder>() {
|
) : RecyclerView.Adapter<ChaptersViewHolder>() {
|
||||||
private var selectedPosition = 0
|
private var selectedPosition = 0
|
||||||
@ -27,15 +29,15 @@ class ChaptersAdapter(
|
|||||||
holder.binding.apply {
|
holder.binding.apply {
|
||||||
ImageHelper.loadImage(chapter.image, chapterImage)
|
ImageHelper.loadImage(chapter.image, chapterImage)
|
||||||
chapterTitle.text = chapter.title
|
chapterTitle.text = chapter.title
|
||||||
|
timeStamp.text = chapter.start?.let { DateUtils.formatElapsedTime(it) }
|
||||||
|
|
||||||
if (selectedPosition == position) {
|
val color = if (selectedPosition == position) {
|
||||||
// get the color for highlighted controls
|
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
|
||||||
val color =
|
|
||||||
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
|
|
||||||
chapterLL.setBackgroundColor(color)
|
|
||||||
} else {
|
} else {
|
||||||
chapterLL.setBackgroundColor(Color.TRANSPARENT)
|
Color.TRANSPARENT
|
||||||
}
|
}
|
||||||
|
chapterLL.setBackgroundColor(color)
|
||||||
|
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
updateSelectedPosition(position)
|
updateSelectedPosition(position)
|
||||||
val chapterStart = chapter.start!! * 1000 // s -> ms
|
val chapterStart = chapter.start!! * 1000 // s -> ms
|
||||||
|
@ -5,6 +5,7 @@ import android.util.Log
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.ClipboardHelper
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
|
import com.github.libretube.util.TextUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -60,7 +62,7 @@ class CommentsAdapter(
|
|||||||
root.scaleY = 0.9f
|
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()
|
commentText.text = comment.commentText.toString()
|
||||||
|
|
||||||
ImageHelper.loadImage(comment.thumbnail, commentorImage)
|
ImageHelper.loadImage(comment.thumbnail, commentorImage)
|
||||||
@ -71,8 +73,7 @@ class CommentsAdapter(
|
|||||||
if (comment.hearted == true) heartedImageView.visibility = View.VISIBLE
|
if (comment.hearted == true) heartedImageView.visibility = View.VISIBLE
|
||||||
if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE
|
if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE
|
||||||
if ((comment.replyCount ?: -1L) > 0L) {
|
if ((comment.replyCount ?: -1L) > 0L) {
|
||||||
repliesCount.text =
|
repliesCount.text = comment.replyCount?.formatShort()
|
||||||
comment.replyCount?.formatShort()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commentorImage.setOnClickListener {
|
commentorImage.setOnClickListener {
|
||||||
@ -89,30 +90,7 @@ class CommentsAdapter(
|
|||||||
repliesRecView.adapter = repliesAdapter
|
repliesRecView.adapter = repliesAdapter
|
||||||
if (!isRepliesAdapter && comment.repliesPage != null) {
|
if (!isRepliesAdapter && comment.repliesPage != null) {
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
when {
|
showMoreReplies(comment.repliesPage, showMore, repliesAdapter)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
override fun getItemCount(): Int {
|
||||||
return comments.size
|
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.activities.OfflinePlayerActivity
|
||||||
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
||||||
import com.github.libretube.util.DownloadHelper
|
import com.github.libretube.util.DownloadHelper
|
||||||
|
import com.github.libretube.util.TextUtils
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ class DownloadsAdapter(
|
|||||||
uploaderName.text = it.uploader
|
uploaderName.text = it.uploader
|
||||||
videoInfo.text = it.views.formatShort() + " " +
|
videoInfo.text = it.views.formatShort() + " " +
|
||||||
root.context.getString(R.string.views_placeholder) +
|
root.context.getString(R.string.views_placeholder) +
|
||||||
" • " + it.uploadDate
|
TextUtils.SEPARATOR + it.uploadDate
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailImage.setImageBitmap(file.thumbnail)
|
thumbnailImage.setImageBitmap(file.thumbnail)
|
||||||
@ -60,8 +61,8 @@ class DownloadsAdapter(
|
|||||||
) { _, index ->
|
) { _, index ->
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
val audioDir = DownloadHelper.getAudioDir(root.context)
|
val audioDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.AUDIO_DIR)
|
||||||
val videoDir = DownloadHelper.getVideoDir(root.context)
|
val videoDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.VIDEO_DIR)
|
||||||
|
|
||||||
listOf(audioDir, videoDir).forEach {
|
listOf(audioDir, videoDir).forEach {
|
||||||
val f = File(it, file.name)
|
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
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.databinding.NavOptionsItemBinding
|
import com.github.libretube.databinding.NavOptionsItemBinding
|
||||||
import com.github.libretube.obj.NavBarItem
|
|
||||||
import com.github.libretube.ui.viewholders.NavBarOptionsViewHolder
|
import com.github.libretube.ui.viewholders.NavBarOptionsViewHolder
|
||||||
|
|
||||||
class NavBarOptionsAdapter(
|
class NavBarOptionsAdapter(
|
||||||
val items: MutableList<NavBarItem>
|
val items: MutableList<MenuItem>
|
||||||
) : RecyclerView.Adapter<NavBarOptionsViewHolder>() {
|
) : RecyclerView.Adapter<NavBarOptionsViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NavBarOptionsViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NavBarOptionsViewHolder {
|
||||||
@ -30,9 +30,9 @@ class NavBarOptionsAdapter(
|
|||||||
val item = items[position]
|
val item = items[position]
|
||||||
holder.binding.apply {
|
holder.binding.apply {
|
||||||
title.text = item.title
|
title.text = item.title
|
||||||
checkbox.isChecked = item.isEnabled
|
checkbox.isChecked = item.isVisible
|
||||||
checkbox.setOnClickListener {
|
checkbox.setOnClickListener {
|
||||||
if (!checkbox.isChecked && getEnabledItemsCount() < 2) {
|
if (!checkbox.isChecked && getVisibleItemsCount() < 2) {
|
||||||
checkbox.isChecked = true
|
checkbox.isChecked = true
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
root.context,
|
root.context,
|
||||||
@ -41,12 +41,12 @@ class NavBarOptionsAdapter(
|
|||||||
).show()
|
).show()
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
items[position].isEnabled = checkbox.isChecked
|
items[position].isVisible = checkbox.isChecked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEnabledItemsCount(): Int {
|
private fun getVisibleItemsCount(): Int {
|
||||||
return items.filter { it.isEnabled }.size
|
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
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.databinding.PlaylistRowBinding
|
||||||
|
import com.github.libretube.enums.PlaylistType
|
||||||
import com.github.libretube.extensions.TAG
|
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.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.sheets.VideoOptionsBottomSheet
|
||||||
import com.github.libretube.ui.viewholders.PlaylistViewHolder
|
import com.github.libretube.ui.viewholders.PlaylistViewHolder
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
import com.github.libretube.util.PreferenceHelper
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class PlaylistAdapter(
|
class PlaylistAdapter(
|
||||||
private val videoFeed: MutableList<com.github.libretube.api.obj.StreamItem>,
|
private val videoFeed: MutableList<StreamItem>,
|
||||||
private val playlistId: String,
|
private val playlistId: String,
|
||||||
private val isOwner: Boolean,
|
private val playlistType: PlaylistType
|
||||||
private val activity: Activity,
|
|
||||||
private val childFragmentManager: FragmentManager
|
|
||||||
) : RecyclerView.Adapter<PlaylistViewHolder>() {
|
) : RecyclerView.Adapter<PlaylistViewHolder>() {
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return videoFeed.size
|
return videoFeed.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateItems(newItems: List<com.github.libretube.api.obj.StreamItem>) {
|
fun updateItems(newItems: List<StreamItem>) {
|
||||||
val oldSize = videoFeed.size
|
val oldSize = videoFeed.size
|
||||||
videoFeed.addAll(newItems)
|
videoFeed.addAll(newItems)
|
||||||
notifyItemRangeInserted(oldSize, videoFeed.size)
|
notifyItemRangeInserted(oldSize, videoFeed.size)
|
||||||
@ -59,40 +58,37 @@ class PlaylistAdapter(
|
|||||||
NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)
|
NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)
|
||||||
}
|
}
|
||||||
val videoId = streamItem.url!!.toID()
|
val videoId = streamItem.url!!.toID()
|
||||||
|
val videoName = streamItem.title!!
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
VideoOptionsBottomSheet(videoId)
|
VideoOptionsBottomSheet(videoId, videoName)
|
||||||
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
|
.show(
|
||||||
|
(root.context as BaseActivity).supportFragmentManager,
|
||||||
|
VideoOptionsBottomSheet::class.java.name
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOwner) {
|
if (playlistType != PlaylistType.PUBLIC) {
|
||||||
deletePlaylist.visibility = View.VISIBLE
|
deletePlaylist.visibility = View.VISIBLE
|
||||||
deletePlaylist.setOnClickListener {
|
deletePlaylist.setOnClickListener {
|
||||||
removeFromPlaylist(position)
|
removeFromPlaylist(root.context, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
|
watchProgress.setWatchProgressLength(videoId, streamItem.duration!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromPlaylist(position: Int) {
|
fun removeFromPlaylist(context: Context, position: Int) {
|
||||||
videoFeed.removeAt(position)
|
videoFeed.removeAt(position)
|
||||||
activity.runOnUiThread { notifyDataSetChanged() }
|
(context as Activity).runOnUiThread {
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
notifyItemRangeChanged(position, itemCount)
|
||||||
|
}
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
RetrofitInstance.authApi.removeFromPlaylist(
|
PlaylistsHelper.removeFromPlaylist(playlistId, position)
|
||||||
PreferenceHelper.getToken(),
|
|
||||||
com.github.libretube.api.obj.PlaylistId(
|
|
||||||
playlistId = playlistId,
|
|
||||||
index = position
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
println(e)
|
Log.e(TAG(), e.toString())
|
||||||
Log.e(TAG(), "IOException, you might not have internet connection")
|
|
||||||
return@launch
|
|
||||||
} catch (e: HttpException) {
|
|
||||||
Log.e(TAG(), "HttpException, unexpected response")
|
|
||||||
return@launch
|
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
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
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.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.sheets.PlaylistOptionsBottomSheet
|
||||||
import com.github.libretube.ui.viewholders.PlaylistsViewHolder
|
import com.github.libretube.ui.viewholders.PlaylistsViewHolder
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NavigationHelper
|
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(
|
class PlaylistsAdapter(
|
||||||
private val playlists: MutableList<com.github.libretube.api.obj.Playlists>,
|
private val playlists: MutableList<Playlists>,
|
||||||
private val childFragmentManager: FragmentManager,
|
private val playlistType: PlaylistType
|
||||||
private val activity: Activity
|
|
||||||
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
|
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return playlists.size
|
return playlists.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateItems(newItems: List<com.github.libretube.api.obj.Playlists>) {
|
fun updateItems(newItems: List<Playlists>) {
|
||||||
val oldSize = playlists.size
|
val oldSize = playlists.size
|
||||||
playlists.addAll(newItems)
|
playlists.addAll(newItems)
|
||||||
notifyItemRangeInserted(oldSize, playlists.size)
|
notifyItemRangeInserted(oldSize, playlists.size)
|
||||||
@ -55,61 +46,37 @@ class PlaylistsAdapter(
|
|||||||
ImageHelper.loadImage(playlist.thumbnail, playlistThumbnail)
|
ImageHelper.loadImage(playlist.thumbnail, playlistThumbnail)
|
||||||
}
|
}
|
||||||
playlistTitle.text = playlist.name
|
playlistTitle.text = playlist.name
|
||||||
|
|
||||||
|
videoCount.text = playlist.videos.toString()
|
||||||
|
|
||||||
deletePlaylist.setOnClickListener {
|
deletePlaylist.setOnClickListener {
|
||||||
val builder = MaterialAlertDialogBuilder(root.context)
|
DeletePlaylistDialog(playlist.id!!, playlistType) {
|
||||||
builder.setTitle(R.string.deletePlaylist)
|
playlists.removeAt(position)
|
||||||
builder.setMessage(R.string.areYouSure)
|
(root.context as BaseActivity).runOnUiThread {
|
||||||
builder.setPositiveButton(R.string.yes) { _, _ ->
|
notifyItemRemoved(position)
|
||||||
PreferenceHelper.getToken()
|
notifyItemRangeChanged(position, itemCount)
|
||||||
deletePlaylist(playlist.id!!, position)
|
}
|
||||||
}
|
}.show(
|
||||||
builder.setNegativeButton(R.string.cancel, null)
|
(root.context as BaseActivity).supportFragmentManager,
|
||||||
builder.show()
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
NavigationHelper.navigatePlaylist(root.context, playlist.id, true)
|
NavigationHelper.navigatePlaylist(root.context, playlist.id, playlistType)
|
||||||
}
|
}
|
||||||
|
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
val playlistOptionsDialog = PlaylistOptionsBottomSheet(
|
val playlistOptionsDialog = PlaylistOptionsBottomSheet(
|
||||||
playlistId = playlist.id!!,
|
playlistId = playlist.id!!,
|
||||||
isOwner = true
|
playlistName = playlist.name!!,
|
||||||
|
playlistType = playlistType
|
||||||
)
|
)
|
||||||
playlistOptionsDialog.show(
|
playlistOptionsDialog.show(
|
||||||
childFragmentManager,
|
(root.context as BaseActivity).supportFragmentManager,
|
||||||
PlaylistOptionsBottomSheet::class.java.name
|
PlaylistOptionsBottomSheet::class.java.name
|
||||||
)
|
)
|
||||||
true
|
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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.SubscriptionHelper
|
import com.github.libretube.api.obj.ContentItem
|
||||||
import com.github.libretube.api.obj.SearchItem
|
|
||||||
import com.github.libretube.databinding.ChannelRowBinding
|
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.databinding.VideoRowBinding
|
||||||
|
import com.github.libretube.enums.PlaylistType
|
||||||
import com.github.libretube.extensions.formatShort
|
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.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.PlaylistOptionsBottomSheet
|
||||||
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
|
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
|
||||||
import com.github.libretube.ui.viewholders.SearchViewHolder
|
import com.github.libretube.ui.viewholders.SearchViewHolder
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import com.github.libretube.util.TextUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SearchAdapter(
|
class SearchAdapter(
|
||||||
private val searchItems: MutableList<SearchItem>,
|
private val searchItems: MutableList<ContentItem>
|
||||||
private val childFragmentManager: FragmentManager
|
|
||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<SearchViewHolder>() {
|
RecyclerView.Adapter<SearchViewHolder>() {
|
||||||
|
|
||||||
fun updateItems(newItems: List<SearchItem>) {
|
fun updateItems(newItems: List<ContentItem>) {
|
||||||
val searchItemsSize = searchItems.size
|
val searchItemsSize = searchItems.size
|
||||||
searchItems.addAll(newItems)
|
searchItems.addAll(newItems)
|
||||||
notifyItemRangeInserted(searchItemsSize, newItems.size)
|
notifyItemRangeInserted(searchItemsSize, newItems.size)
|
||||||
@ -52,7 +50,7 @@ class SearchAdapter(
|
|||||||
ChannelRowBinding.inflate(layoutInflater, parent, false)
|
ChannelRowBinding.inflate(layoutInflater, parent, false)
|
||||||
)
|
)
|
||||||
2 -> SearchViewHolder(
|
2 -> SearchViewHolder(
|
||||||
PlaylistSearchRowBinding.inflate(layoutInflater, parent, false)
|
PlaylistsRowBinding.inflate(layoutInflater, parent, false)
|
||||||
)
|
)
|
||||||
else -> throw IllegalArgumentException("Invalid type")
|
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 {
|
binding.apply {
|
||||||
ImageHelper.loadImage(item.thumbnail, thumbnail)
|
ImageHelper.loadImage(item.thumbnail, thumbnail)
|
||||||
thumbnailDuration.setFormattedDuration(item.duration!!)
|
thumbnailDuration.setFormattedDuration(item.duration!!)
|
||||||
@ -100,12 +98,13 @@ class SearchAdapter(
|
|||||||
NavigationHelper.navigateVideo(root.context, item.url)
|
NavigationHelper.navigateVideo(root.context, item.url)
|
||||||
}
|
}
|
||||||
val videoId = item.url!!.toID()
|
val videoId = item.url!!.toID()
|
||||||
|
val videoName = item.title!!
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
VideoOptionsBottomSheet(videoId)
|
VideoOptionsBottomSheet(videoId, videoName)
|
||||||
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
|
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
channelImage.setOnClickListener {
|
channelContainer.setOnClickListener {
|
||||||
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
|
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
|
||||||
}
|
}
|
||||||
watchProgress.setWatchProgressLength(videoId, item.duration!!)
|
watchProgress.setWatchProgressLength(videoId, item.duration!!)
|
||||||
@ -114,7 +113,7 @@ class SearchAdapter(
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
private fun bindChannel(
|
private fun bindChannel(
|
||||||
item: SearchItem,
|
item: ContentItem,
|
||||||
binding: ChannelRowBinding
|
binding: ChannelRowBinding
|
||||||
) {
|
) {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
@ -123,66 +122,33 @@ class SearchAdapter(
|
|||||||
searchViews.text = root.context.getString(
|
searchViews.text = root.context.getString(
|
||||||
R.string.subscribers,
|
R.string.subscribers,
|
||||||
item.subscribers.formatShort()
|
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 {
|
root.setOnClickListener {
|
||||||
NavigationHelper.navigateChannel(root.context, item.url)
|
NavigationHelper.navigateChannel(root.context, item.url)
|
||||||
}
|
}
|
||||||
val channelId = item.url!!.toID()
|
|
||||||
|
|
||||||
isSubscribed(channelId, binding)
|
binding.searchSubButton.setupSubscriptionButton(item.url?.toID(), item.name?.toID())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindPlaylist(
|
private fun bindPlaylist(
|
||||||
item: SearchItem,
|
item: ContentItem,
|
||||||
binding: PlaylistSearchRowBinding
|
binding: PlaylistsRowBinding
|
||||||
) {
|
) {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
ImageHelper.loadImage(item.thumbnail, searchThumbnail)
|
ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
|
||||||
if (item.videos?.toInt() != -1) searchPlaylistNumber.text = item.videos.toString()
|
if (item.videos?.toInt() != -1) videoCount.text = item.videos.toString()
|
||||||
searchDescription.text = item.name
|
playlistTitle.text = item.name
|
||||||
searchName.text = item.uploaderName
|
playlistDescription.text = item.uploaderName
|
||||||
if (item.videos?.toInt() != -1) {
|
|
||||||
searchPlaylistVideos.text =
|
|
||||||
root.context.getString(R.string.videoCount, item.videos.toString())
|
|
||||||
}
|
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
NavigationHelper.navigatePlaylist(root.context, item.url, false)
|
NavigationHelper.navigatePlaylist(root.context, item.url, PlaylistType.PUBLIC)
|
||||||
}
|
}
|
||||||
|
deletePlaylist.visibility = View.GONE
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
val playlistId = item.url!!.toID()
|
val playlistId = item.url!!.toID()
|
||||||
PlaylistOptionsBottomSheet(playlistId, false)
|
val playlistName = item.name!!
|
||||||
.show(childFragmentManager, PlaylistOptionsBottomSheet::class.java.name)
|
PlaylistOptionsBottomSheet(playlistId, playlistName, PlaylistType.PUBLIC)
|
||||||
|
.show((root.context as BaseActivity).supportFragmentManager, PlaylistOptionsBottomSheet::class.java.name)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,9 @@ class SearchSuggestionsAdapter(
|
|||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
searchView.setQuery(suggestion, true)
|
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.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.api.obj.Subscription
|
||||||
import com.github.libretube.api.SubscriptionHelper
|
|
||||||
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
|
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
|
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||||
import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
|
import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
|
|
||||||
class SubscriptionChannelAdapter(private val subscriptions: MutableList<com.github.libretube.api.obj.Subscription>) :
|
class SubscriptionChannelAdapter(
|
||||||
RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
|
private val subscriptions: MutableList<Subscription>
|
||||||
|
) : RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return subscriptions.size
|
return subscriptions.size
|
||||||
@ -27,27 +28,20 @@ class SubscriptionChannelAdapter(private val subscriptions: MutableList<com.gith
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
|
||||||
val subscription = subscriptions[position]
|
val subscription = subscriptions[position]
|
||||||
var subscribed = true
|
|
||||||
|
|
||||||
holder.binding.apply {
|
holder.binding.apply {
|
||||||
subscriptionChannelName.text = subscription.name
|
subscriptionChannelName.text = subscription.name
|
||||||
ImageHelper.loadImage(subscription.avatar, subscriptionChannelImage)
|
ImageHelper.loadImage(subscription.avatar, subscriptionChannelImage)
|
||||||
|
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
NavigationHelper.navigateChannel(root.context, subscription.url)
|
NavigationHelper.navigateChannel(root.context, subscription.url)
|
||||||
}
|
}
|
||||||
subscriptionSubscribe.setOnClickListener {
|
subscriptionSubscribe.setupSubscriptionButton(
|
||||||
val channelId = subscription.url!!.toID()
|
subscription.url?.toID(),
|
||||||
if (subscribed) {
|
subscription.name,
|
||||||
subscriptionSubscribe.text = root.context.getString(R.string.subscribe)
|
notificationBell,
|
||||||
SubscriptionHelper.unsubscribe(channelId)
|
true
|
||||||
subscribed = false
|
)
|
||||||
} else {
|
|
||||||
subscriptionSubscribe.text =
|
|
||||||
root.context.getString(R.string.unsubscribe)
|
|
||||||
SubscriptionHelper.subscribe(channelId)
|
|
||||||
subscribed = 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.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.databinding.WatchHistoryRowBinding
|
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.db.obj.WatchHistoryItem
|
||||||
import com.github.libretube.extensions.setFormattedDuration
|
import com.github.libretube.extensions.query
|
||||||
import com.github.libretube.extensions.setWatchProgressLength
|
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.sheets.VideoOptionsBottomSheet
|
||||||
import com.github.libretube.ui.viewholders.WatchHistoryViewHolder
|
import com.github.libretube.ui.viewholders.WatchHistoryViewHolder
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
|
|
||||||
class WatchHistoryAdapter(
|
class WatchHistoryAdapter(
|
||||||
private val watchHistory: MutableList<WatchHistoryItem>,
|
private val watchHistory: MutableList<WatchHistoryItem>
|
||||||
private val childFragmentManager: FragmentManager
|
|
||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<WatchHistoryViewHolder>() {
|
RecyclerView.Adapter<WatchHistoryViewHolder>() {
|
||||||
|
|
||||||
fun removeFromWatchHistory(position: Int) {
|
fun removeFromWatchHistory(position: Int) {
|
||||||
DatabaseHelper.removeFromWatchHistory(position)
|
query {
|
||||||
|
DatabaseHolder.Database.watchHistoryDao().delete(watchHistory[position])
|
||||||
|
}
|
||||||
watchHistory.removeAt(position)
|
watchHistory.removeAt(position)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
notifyItemRangeChanged(position, itemCount)
|
notifyItemRangeChanged(position, itemCount)
|
||||||
@ -55,8 +57,8 @@ class WatchHistoryAdapter(
|
|||||||
NavigationHelper.navigateVideo(root.context, video.videoId)
|
NavigationHelper.navigateVideo(root.context, video.videoId)
|
||||||
}
|
}
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
VideoOptionsBottomSheet(video.videoId)
|
VideoOptionsBottomSheet(video.videoId, video.title!!)
|
||||||
.show(childFragmentManager, VideoOptionsBottomSheet::class.java.name)
|
.show((root.context as BaseActivity).supportFragmentManager, VideoOptionsBottomSheet::class.java.name)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
package com.github.libretube.ui.dialogs
|
package com.github.libretube.ui.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -13,36 +10,34 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.PlaylistsHelper
|
||||||
import com.github.libretube.api.obj.PlaylistId
|
|
||||||
import com.github.libretube.constants.IntentData
|
|
||||||
import com.github.libretube.databinding.DialogAddtoplaylistBinding
|
import com.github.libretube.databinding.DialogAddtoplaylistBinding
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.models.PlaylistViewModel
|
import com.github.libretube.extensions.toastFromMainThread
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.ui.models.PlaylistViewModel
|
||||||
import com.github.libretube.util.ThemeHelper
|
import com.github.libretube.util.ThemeHelper
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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 lateinit var binding: DialogAddtoplaylistBinding
|
||||||
private val viewModel: PlaylistViewModel by activityViewModels()
|
private val viewModel: PlaylistViewModel by activityViewModels()
|
||||||
|
|
||||||
private lateinit var videoId: String
|
|
||||||
private lateinit var token: String
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
videoId = arguments?.getString(IntentData.videoId)!!
|
|
||||||
binding = DialogAddtoplaylistBinding.inflate(layoutInflater)
|
binding = DialogAddtoplaylistBinding.inflate(layoutInflater)
|
||||||
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
|
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
|
||||||
|
|
||||||
token = PreferenceHelper.getToken()
|
binding.createPlaylist.setOnClickListener {
|
||||||
|
CreatePlaylistDialog {
|
||||||
|
fetchPlaylists()
|
||||||
|
}.show(childFragmentManager, null)
|
||||||
|
}
|
||||||
|
|
||||||
if (token != "") fetchPlaylists()
|
fetchPlaylists()
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
@ -52,16 +47,11 @@ class AddToPlaylistDialog : DialogFragment() {
|
|||||||
private fun fetchPlaylists() {
|
private fun fetchPlaylists() {
|
||||||
lifecycleScope.launchWhenCreated {
|
lifecycleScope.launchWhenCreated {
|
||||||
val response = try {
|
val response = try {
|
||||||
RetrofitInstance.authApi.playlists(token)
|
PlaylistsHelper.getPlaylists()
|
||||||
} catch (e: IOException) {
|
} catch (e: Exception) {
|
||||||
println(e)
|
Log.e(TAG(), e.toString())
|
||||||
Log.e(TAG(), "IOException, you might not have internet connection")
|
|
||||||
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
|
||||||
return@launchWhenCreated
|
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()) {
|
if (response.isNotEmpty()) {
|
||||||
val names = response.map { it.name }
|
val names = response.map { it.name }
|
||||||
@ -75,8 +65,7 @@ class AddToPlaylistDialog : DialogFragment() {
|
|||||||
var selectionIndex = 0
|
var selectionIndex = 0
|
||||||
response.forEachIndexed { index, playlist ->
|
response.forEachIndexed { index, playlist ->
|
||||||
if (playlist.id == viewModel.lastSelectedPlaylistId) {
|
if (playlist.id == viewModel.lastSelectedPlaylistId) {
|
||||||
selectionIndex =
|
selectionIndex = index
|
||||||
index
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.playlistsSpinner.setSelection(selectionIndex)
|
binding.playlistsSpinner.setSelection(selectionIndex)
|
||||||
@ -96,38 +85,19 @@ class AddToPlaylistDialog : DialogFragment() {
|
|||||||
private fun addToPlaylist(playlistId: String) {
|
private fun addToPlaylist(playlistId: String) {
|
||||||
val appContext = context?.applicationContext ?: return
|
val appContext = context?.applicationContext ?: return
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val response = try {
|
val success = try {
|
||||||
RetrofitInstance.authApi.addToPlaylist(
|
PlaylistsHelper.addToPlaylist(playlistId, videoId)
|
||||||
token,
|
} catch (e: Exception) {
|
||||||
PlaylistId(playlistId, videoId)
|
Log.e(TAG(), e.toString())
|
||||||
)
|
appContext.toastFromMainThread(R.string.unknown_error)
|
||||||
} 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)
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
toastFromMainThread(
|
appContext.toastFromMainThread(
|
||||||
appContext,
|
if (success) R.string.added_to_playlist else R.string.fail
|
||||||
if (response.message == "ok") 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) {
|
private fun Fragment?.runOnUiThread(action: () -> Unit) {
|
||||||
this ?: return
|
this ?: return
|
||||||
if (!isAdded) return // Fragment not attached to an Activity
|
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.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
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.BackupFile
|
||||||
import com.github.libretube.obj.PreferenceItem
|
import com.github.libretube.obj.PreferenceItem
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
@ -13,21 +15,56 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
class BackupDialog(
|
class BackupDialog(
|
||||||
private val createBackupFile: (BackupFile) -> Unit
|
private val createBackupFile: (BackupFile) -> Unit
|
||||||
) : DialogFragment() {
|
) : 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 {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val backupOptionNames = listOf(
|
val backupOptions = listOf(
|
||||||
R.string.watch_history,
|
BackupOption.WatchHistory,
|
||||||
R.string.watch_positions,
|
BackupOption.WatchPositions,
|
||||||
R.string.search_history,
|
BackupOption.SearchHistory,
|
||||||
R.string.local_subscriptions,
|
BackupOption.LocalSubscriptions,
|
||||||
R.string.backup_customInstances,
|
BackupOption.CustomInstances,
|
||||||
R.string.preferences
|
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())
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.backup)
|
.setTitle(R.string.backup)
|
||||||
@ -36,39 +73,12 @@ class BackupDialog(
|
|||||||
}
|
}
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.backup) { _, _ ->
|
.setPositiveButton(R.string.backup) { _, _ ->
|
||||||
val thread = Thread {
|
val backupFile = BackupFile()
|
||||||
if (selected[0]) {
|
awaitQuery {
|
||||||
backupFile.watchHistory =
|
backupOptions.forEachIndexed { index, option ->
|
||||||
Database.watchHistoryDao().getAll()
|
if (selected[index]) option.onSelected.invoke(backupFile)
|
||||||
}
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thread.start()
|
|
||||||
thread.join()
|
|
||||||
|
|
||||||
createBackupFile(backupFile)
|
createBackupFile(backupFile)
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
@ -2,23 +2,18 @@ package com.github.libretube.ui.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.R
|
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.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.github.libretube.util.ThemeHelper
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class CreatePlaylistDialog : DialogFragment() {
|
class CreatePlaylistDialog(
|
||||||
private var token: String = ""
|
private val onSuccess: () -> Unit = {}
|
||||||
|
) : DialogFragment() {
|
||||||
private lateinit var binding: DialogCreatePlaylistBinding
|
private lateinit var binding: DialogCreatePlaylistBinding
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@ -30,14 +25,17 @@ class CreatePlaylistDialog : DialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
token = PreferenceHelper.getToken()
|
|
||||||
|
|
||||||
binding.createNewPlaylist.setOnClickListener {
|
binding.createNewPlaylist.setOnClickListener {
|
||||||
// avoid creating the same playlist multiple times by spamming the button
|
// avoid creating the same playlist multiple times by spamming the button
|
||||||
binding.createNewPlaylist.setOnClickListener(null)
|
binding.createNewPlaylist.setOnClickListener(null)
|
||||||
val listName = binding.playlistName.text.toString()
|
val listName = binding.playlistName.text.toString()
|
||||||
if (listName != "") {
|
if (listName != "") {
|
||||||
createPlaylist(listName)
|
lifecycleScope.launchWhenCreated {
|
||||||
|
PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext) {
|
||||||
|
onSuccess.invoke()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
@ -47,38 +45,4 @@ class CreatePlaylistDialog : DialogFragment() {
|
|||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.show()
|
.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)
|
.setTitle(R.string.navigation_bar)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.setPositiveButton(R.string.okay) { _, _ ->
|
.setPositiveButton(R.string.okay) { _, _ ->
|
||||||
NavBarHelper.setNavBarItems(adapter.items)
|
NavBarHelper.setNavBarItems(adapter.items, requireContext())
|
||||||
RequireRestartDialog()
|
RequireRestartDialog()
|
||||||
.show(requireParentFragment().childFragmentManager, null)
|
.show(requireParentFragment().childFragmentManager, null)
|
||||||
}
|
}
|
||||||
|
@ -8,18 +8,19 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.constants.PIPED_FRONTEND_URL
|
import com.github.libretube.constants.PIPED_FRONTEND_URL
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.constants.ShareObjectType
|
|
||||||
import com.github.libretube.constants.YOUTUBE_FRONTEND_URL
|
import com.github.libretube.constants.YOUTUBE_FRONTEND_URL
|
||||||
import com.github.libretube.databinding.DialogShareBinding
|
import com.github.libretube.databinding.DialogShareBinding
|
||||||
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
||||||
|
import com.github.libretube.enums.ShareObjectType
|
||||||
import com.github.libretube.extensions.awaitQuery
|
import com.github.libretube.extensions.awaitQuery
|
||||||
|
import com.github.libretube.obj.ShareData
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
class ShareDialog(
|
class ShareDialog(
|
||||||
private val id: String,
|
private val id: String,
|
||||||
private val shareObjectType: Int,
|
private val shareObjectType: ShareObjectType,
|
||||||
private val position: Long? = null
|
private val shareData: ShareData
|
||||||
) : DialogFragment() {
|
) : DialogFragment() {
|
||||||
private var binding: DialogShareBinding? = null
|
private var binding: DialogShareBinding? = null
|
||||||
|
|
||||||
@ -29,11 +30,11 @@ class ShareDialog(
|
|||||||
getString(R.string.youtube)
|
getString(R.string.youtube)
|
||||||
)
|
)
|
||||||
val instanceUrl = getCustomInstanceFrontendUrl()
|
val instanceUrl = getCustomInstanceFrontendUrl()
|
||||||
|
val shareableTitle = getShareableTitle(shareData)
|
||||||
// add instanceUrl option if custom instance frontend url available
|
// add instanceUrl option if custom instance frontend url available
|
||||||
if (instanceUrl != "") shareOptions += getString(R.string.instance)
|
if (instanceUrl != "") shareOptions += getString(R.string.instance)
|
||||||
|
|
||||||
if (shareObjectType == ShareObjectType.VIDEO && position != null) {
|
if (shareObjectType == ShareObjectType.VIDEO) {
|
||||||
setupTimeStampBinding()
|
setupTimeStampBinding()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ class ShareDialog(
|
|||||||
}
|
}
|
||||||
var url = "$host$path"
|
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}"
|
url += "&t=${binding!!.timeStamp.text}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ class ShareDialog(
|
|||||||
intent.apply {
|
intent.apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, shareableTitle)
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
}
|
}
|
||||||
context?.startActivity(
|
context?.startActivity(
|
||||||
@ -81,8 +83,9 @@ class ShareDialog(
|
|||||||
)
|
)
|
||||||
binding!!.timeCodeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
binding!!.timeCodeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding!!.timeStampLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
|
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
|
if (binding!!.timeCodeSwitch.isChecked) binding!!.timeStampLayout.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,4 +107,18 @@ class ShareDialog(
|
|||||||
}
|
}
|
||||||
return ""
|
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.text.format.DateUtils
|
||||||
import android.widget.TextView
|
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.View
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
||||||
|
import com.github.libretube.extensions.awaitQuery
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shows the already watched time under the video
|
* 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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.SubscriptionHelper
|
import com.github.libretube.api.SubscriptionHelper
|
||||||
|
import com.github.libretube.api.obj.ChannelTab
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.ShareObjectType
|
|
||||||
import com.github.libretube.databinding.FragmentChannelBinding
|
import com.github.libretube.databinding.FragmentChannelBinding
|
||||||
|
import com.github.libretube.enums.ShareObjectType
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.extensions.formatShort
|
import com.github.libretube.extensions.formatShort
|
||||||
import com.github.libretube.extensions.toID
|
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.base.BaseFragment
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
|
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@ -29,12 +38,22 @@ class ChannelFragment : BaseFragment() {
|
|||||||
|
|
||||||
private var channelId: String? = null
|
private var channelId: String? = null
|
||||||
private var channelName: String? = null
|
private var channelName: String? = null
|
||||||
|
|
||||||
private var nextPage: String? = null
|
private var nextPage: String? = null
|
||||||
private var channelAdapter: ChannelAdapter? = null
|
private var channelAdapter: VideosAdapter? = null
|
||||||
private var isLoading = true
|
private var isLoading = true
|
||||||
private var isSubscribed: Boolean? = false
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
arguments?.let {
|
arguments?.let {
|
||||||
@ -63,7 +82,9 @@ class ChannelFragment : BaseFragment() {
|
|||||||
binding.channelRefresh.isRefreshing = true
|
binding.channelRefresh.isRefreshing = true
|
||||||
fetchChannel()
|
fetchChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshChannel()
|
refreshChannel()
|
||||||
|
|
||||||
binding.channelRefresh.setOnRefreshListener {
|
binding.channelRefresh.setOnRefreshListener {
|
||||||
refreshChannel()
|
refreshChannel()
|
||||||
}
|
}
|
||||||
@ -73,11 +94,10 @@ class ChannelFragment : BaseFragment() {
|
|||||||
if (binding.channelScrollView.getChildAt(0).bottom
|
if (binding.channelScrollView.getChildAt(0).bottom
|
||||||
== (binding.channelScrollView.height + binding.channelScrollView.scrollY)
|
== (binding.channelScrollView.height + binding.channelScrollView.scrollY)
|
||||||
) {
|
) {
|
||||||
// scroll view is at bottom
|
try {
|
||||||
if (nextPage != null && !isLoading) {
|
onScrollEnd.invoke()
|
||||||
isLoading = true
|
} catch (e: Exception) {
|
||||||
binding.channelRefresh.isRefreshing = true
|
Log.e("tabs failed", e.toString())
|
||||||
fetchChannelNextPage()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,30 +122,26 @@ class ChannelFragment : BaseFragment() {
|
|||||||
}
|
}
|
||||||
// needed if the channel gets loaded by the ID
|
// needed if the channel gets loaded by the ID
|
||||||
channelId = response.id
|
channelId = response.id
|
||||||
|
channelName = response.name
|
||||||
|
val shareData = ShareData(currentChannel = response.name)
|
||||||
|
|
||||||
|
onScrollEnd = {
|
||||||
|
fetchChannelNextPage()
|
||||||
|
}
|
||||||
|
|
||||||
// fetch and update the subscription status
|
// fetch and update the subscription status
|
||||||
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
|
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
|
||||||
if (isSubscribed == null) return@launchWhenCreated
|
if (isSubscribed == null) return@launchWhenCreated
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (isSubscribed == true) {
|
binding.channelSubscribe.setupSubscriptionButton(channelId, channelName, binding.notificationBell)
|
||||||
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.channelShare.setOnClickListener {
|
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)
|
shareDialog.show(childFragmentManager, ShareDialog::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,23 +181,82 @@ class ChannelFragment : BaseFragment() {
|
|||||||
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
|
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
|
||||||
|
|
||||||
// recyclerview of the videos by the channel
|
// recyclerview of the videos by the channel
|
||||||
channelAdapter = ChannelAdapter(
|
channelAdapter = VideosAdapter(
|
||||||
response.relatedStreams!!.toMutableList(),
|
response.relatedStreams.orEmpty().toMutableList(),
|
||||||
childFragmentManager
|
forceMode = VideosAdapter.Companion.ForceMode.CHANNEL
|
||||||
)
|
)
|
||||||
binding.channelRecView.adapter = channelAdapter
|
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() {
|
private fun fetchChannelNextPage() {
|
||||||
fun run() {
|
fun run() {
|
||||||
|
if (nextPage == null || isLoading) return
|
||||||
|
isLoading = true
|
||||||
|
binding.channelRefresh.isRefreshing = true
|
||||||
|
|
||||||
lifecycleScope.launchWhenCreated {
|
lifecycleScope.launchWhenCreated {
|
||||||
val response = try {
|
val response = try {
|
||||||
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
|
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
binding.channelRefresh.isRefreshing = false
|
binding.channelRefresh.isRefreshing = false
|
||||||
println(e)
|
|
||||||
Log.e(TAG(), "IOException, you might not have internet connection")
|
Log.e(TAG(), "IOException, you might not have internet connection")
|
||||||
return@launchWhenCreated
|
return@launchWhenCreated
|
||||||
} catch (e: HttpException) {
|
} catch (e: HttpException) {
|
||||||
@ -190,11 +265,33 @@ class ChannelFragment : BaseFragment() {
|
|||||||
return@launchWhenCreated
|
return@launchWhenCreated
|
||||||
}
|
}
|
||||||
nextPage = response.nextpage
|
nextPage = response.nextpage
|
||||||
channelAdapter?.updateItems(response.relatedStreams!!)
|
channelAdapter?.insertItems(response.relatedStreams.orEmpty())
|
||||||
isLoading = false
|
isLoading = false
|
||||||
binding.channelRefresh.isRefreshing = false
|
binding.channelRefresh.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run()
|
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