diff --git a/.github/tg.py b/.github/tg.py
index 0c99f83ba..77edb7b29 100644
--- a/.github/tg.py
+++ b/.github/tg.py
@@ -1,33 +1,34 @@
-import telegram
-from tgconfig import *
-from json import load
-import multiprocessing
-from os import system
-from time import sleep as wait
-
-def deploy():
- system(f'./exec --local --api-id={TG_API_ID} --api-hash={TG_API_HASH}')
-
-def bot():
- wait(10)
- f = open('commit.json')
- data = load(f)
- f.close()
-
- bot = telegram.Bot(TG_TOKEN, base_url="http://0.0.0.0:8081/bot")
- bot.send_photo(TG_POST_ID, open('alpha.png', 'rb'), f'''*Libretube {data['sha'][0:7]} // Alpha*
-
-[{data['commit']['message']}]({data['html_url']})
-
-Signed-off-by: {data['commit']['author']['name']}
-''', parse_mode=telegram.ParseMode.MARKDOWN)
- bot.send_media_group(TG_POST_ID, [telegram.InputMediaDocument(open('app-universal-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-x86-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-x86_64-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-armeabi-v7a-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-arm64-v8a-debug.apk', 'rb'))])
- system('killall -9 python')
-
-if __name__ == '__main__':
- multideploy = multiprocessing.Process(target=deploy)
- multibot = multiprocessing.Process(target=bot)
- multideploy.start()
- multibot.start()
- multideploy.join()
- multibot.join()
+import asyncio
+from json import load
+from os import listdir
+
+from pyrogram import Client
+from pyrogram.types import InputMediaDocument
+from tgconfig import *
+
+files = listdir()
+
+mediadocuments = [
+ InputMediaDocument(file) for file in files if file.endswith("signed.apk")
+]
+
+with open("commit.json") as f:
+ data = load(f)
+
+caption = f"""**Libretube {data['sha'][0:7]} // Alpha**
+
+{data['commit']['message']}
+
+Signed-off-by: {data['commit']['author']['name']}
+"""
+
+
+async def main():
+ async with Client("libretube", TG_API_ID, TG_API_HASH, bot_token=TG_TOKEN) as app:
+ await app.send_photo(
+ int(TG_POST_ID), "https://libre-tube.github.io/images/Alpha.png", caption
+ )
+ await app.send_media_group(int(TG_POST_ID), mediadocuments)
+
+
+asyncio.run(main())
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c86d6a6c6..248a8c89c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v4
with:
python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -54,6 +54,18 @@ jobs:
cd ..
./gradlew assembleDebug
+ - name: Sign Apk
+ continue-on-error: true
+ id: sign_apk
+ uses: ilharp/sign-android-release@v1
+ with:
+ releaseDir: app/build/outputs/apk/debug
+ signingKey: ${{ secrets.ANDROID_SIGNING_KEY }}
+ keyAlias: ${{ secrets.ANDROID_KEY_ALIAS }}
+ keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
+ keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
+
+
- name: Upload to Archive
continue-on-error: true
run: |
@@ -61,7 +73,7 @@ jobs:
echo "GH_REPO = '${{ github.repository }}'" > tgconfig.py
git clone https://github.com/LibreTubeAlpha/Archive archive
rm -rf archive/*.apk
- mv app/build/outputs/apk/debug/*.apk archive/
+ mv app/build/outputs/apk/debug/*-signed.apk archive/
cd archive
python ../uploader.py
@@ -69,15 +81,12 @@ jobs:
continue-on-error: true
run: |
cd archive
- curl https://libre-tube.github.io/images/Alpha.png --output alpha.png
- chmod 755 ./exec
mv ../tgconfig.py .
echo "TG_TOKEN = '${{ secrets.TG_TOKEN }}'" >> tgconfig.py
echo "TG_API_ID = '${{ secrets.TG_API_ID }}'" >> tgconfig.py
echo "TG_POST_ID = '${{ secrets.TG_POST_ID }}'" >> tgconfig.py
echo "TG_API_HASH = '${{ secrets.TG_API_HASH }}'" >> tgconfig.py
- python -m pip install --upgrade pip
- pip install python-telegram-bot
+ python -m pip install --upgrade pip TgCrypto Pyrogram
mv ../.github/tg.py .
mv ../.github/commit.json .
python tg.py
@@ -86,4 +95,5 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: app
- path: archive/*.apk
+ path: archive/*-signed.apk
+
diff --git a/README.md b/README.md
index a112cfb4f..6b870061c 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,7 @@ If creating a pull request, please make sure to format your code (preferred ktli
-## 🌗 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.
diff --git a/ROADMAP.md b/ROADMAP.md
index c2dea6b39..bc083c42f 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,8 +6,7 @@ This represents the larger, bigger impact features and enhancements we have plan
Feel free to help us if you have any knowledge concerning the following planned features or anything else you imagine.
## Planned
-- Support for local playlists
-- Various smaller features
+- Currently only various smaller features
## Not planned
- Google/MicroG Login
diff --git a/app/build.gradle b/app/build.gradle
index 6b2a3c870..9b32ddf4f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,11 +13,17 @@ android {
applicationId 'com.github.libretube'
minSdk 21
targetSdk 33
- versionCode 20
- versionName '0.6.1'
+ versionCode 23
+ versionName '0.8.0'
multiDexEnabled true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
resValue "string", "app_name", "LibreTube"
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
+ }
+ }
}
buildFeatures {
@@ -41,6 +47,8 @@ android {
}
debug {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable true
applicationIdSuffix ".debug"
resValue "string", "app_name", "LibreTube Debug"
@@ -101,6 +109,7 @@ dependencies {
implementation libs.exoplayer
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
implementation libs.exoplayer.extension.mediasession
+ implementation libs.exoplayer.dash
/* Retrofit and Jackson */
implementation libs.square.retrofit
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index c035dfb82..db9143d05 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -16,15 +16,16 @@
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable
+# prevents obfuscation in debug logs
+-dontobfuscate
+
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+
#uncomment for debug
#-keepnames class **
+
+# Keep data classes used for Retrofit
-keep class com.github.libretube.obj.** { *; }
-
-# prevents android from removing it
--keep class com.github.libretube.obj.**.** { *; }
-
-# prevents obfuscation in debug logs
--dontobfuscate
\ No newline at end of file
+-keep class com.github.libretube.obj.update.** { *; }
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
index e1a3264e9..7e8abec17 100644
--- a/app/release/output-metadata.json
+++ b/app/release/output-metadata.json
@@ -11,36 +11,10 @@
"type": "UNIVERSAL",
"filters": [],
"attributes": [],
- "versionCode": 20,
- "versionName": "0.6.1",
+ "versionCode": 23,
+ "versionName": "0.8.0",
"outputFile": "app-universal-release.apk"
},
- {
- "type": "ONE_OF_MANY",
- "filters": [
- {
- "filterType": "ABI",
- "value": "arm64-v8a"
- }
- ],
- "attributes": [],
- "versionCode": 20,
- "versionName": "0.6.1",
- "outputFile": "app-arm64-v8a-release.apk"
- },
- {
- "type": "ONE_OF_MANY",
- "filters": [
- {
- "filterType": "ABI",
- "value": "armeabi-v7a"
- }
- ],
- "attributes": [],
- "versionCode": 20,
- "versionName": "0.6.1",
- "outputFile": "app-armeabi-v7a-release.apk"
- },
{
"type": "ONE_OF_MANY",
"filters": [
@@ -50,10 +24,23 @@
}
],
"attributes": [],
- "versionCode": 20,
- "versionName": "0.6.1",
+ "versionCode": 23,
+ "versionName": "0.8.0",
"outputFile": "app-x86-release.apk"
},
+ {
+ "type": "ONE_OF_MANY",
+ "filters": [
+ {
+ "filterType": "ABI",
+ "value": "arm64-v8a"
+ }
+ ],
+ "attributes": [],
+ "versionCode": 23,
+ "versionName": "0.8.0",
+ "outputFile": "app-arm64-v8a-release.apk"
+ },
{
"type": "ONE_OF_MANY",
"filters": [
@@ -63,9 +50,22 @@
}
],
"attributes": [],
- "versionCode": 20,
- "versionName": "0.6.1",
+ "versionCode": 23,
+ "versionName": "0.8.0",
"outputFile": "app-x86_64-release.apk"
+ },
+ {
+ "type": "ONE_OF_MANY",
+ "filters": [
+ {
+ "filterType": "ABI",
+ "value": "armeabi-v7a"
+ }
+ ],
+ "attributes": [],
+ "versionCode": 23,
+ "versionName": "0.8.0",
+ "outputFile": "app-armeabi-v7a-release.apk"
}
],
"elementType": "File"
diff --git a/app/schemas/com.github.libretube.db.AppDatabase/7.json b/app/schemas/com.github.libretube.db.AppDatabase/7.json
new file mode 100644
index 000000000..17bde0ca2
--- /dev/null
+++ b/app/schemas/com.github.libretube.db.AppDatabase/7.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.libretube.db.AppDatabase/8.json b/app/schemas/com.github.libretube.db.AppDatabase/8.json
new file mode 100644
index 000000000..5d8465e8c
--- /dev/null
+++ b/app/schemas/com.github.libretube.db.AppDatabase/8.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.github.libretube.db.AppDatabase/9.json b/app/schemas/com.github.libretube.db.AppDatabase/9.json
new file mode 100644
index 000000000..b1129fef3
--- /dev/null
+++ b/app/schemas/com.github.libretube.db.AppDatabase/9.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 02a10b7ca..925a61dcf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
+
+
+
@@ -20,7 +27,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/StartupTheme"
- tools:targetApi="n">
+ tools:targetApi="n"
+ android:banner="@mipmap/ic_launcher">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -329,4 +385,4 @@
android:exported="false" />
-
\ No newline at end of file
+
diff --git a/app/src/main/ic_launcher_light-playstore.png b/app/src/main/ic_launcher_light-playstore.png
new file mode 100644
index 000000000..df4f382f6
Binary files /dev/null and b/app/src/main/ic_launcher_light-playstore.png differ
diff --git a/app/src/main/java/com/github/libretube/LibreTubeApp.kt b/app/src/main/java/com/github/libretube/LibreTubeApp.kt
index 77212a998..dbd4db631 100644
--- a/app/src/main/java/com/github/libretube/LibreTubeApp.kt
+++ b/app/src/main/java/com/github/libretube/LibreTubeApp.kt
@@ -16,6 +16,7 @@ import com.github.libretube.util.ExceptionHandler
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NotificationHelper
import com.github.libretube.util.PreferenceHelper
+import com.github.libretube.util.ProxyHelper
class LibreTubeApp : Application() {
override fun onCreate() {
@@ -52,10 +53,16 @@ class LibreTubeApp : Application() {
/**
* Initialize the notification listener in the background
*/
- NotificationHelper(this).enqueueWork(
+ NotificationHelper.enqueueWork(
+ context = this,
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP
)
+ /**
+ * Fetch the image proxy URL for local playlists and the watch history
+ */
+ ProxyHelper.fetchProxyUrl()
+
/**
* Handler for uncaught exceptions
*/
diff --git a/app/src/main/java/com/github/libretube/api/PipedApi.kt b/app/src/main/java/com/github/libretube/api/PipedApi.kt
index a6eb42c6d..cfdf4457b 100644
--- a/app/src/main/java/com/github/libretube/api/PipedApi.kt
+++ b/app/src/main/java/com/github/libretube/api/PipedApi.kt
@@ -1,5 +1,22 @@
package com.github.libretube.api
+import com.github.libretube.api.obj.Channel
+import com.github.libretube.api.obj.ChannelTabResponse
+import com.github.libretube.api.obj.CommentsPage
+import com.github.libretube.api.obj.DeleteUserRequest
+import com.github.libretube.api.obj.Login
+import com.github.libretube.api.obj.Message
+import com.github.libretube.api.obj.PipedConfig
+import com.github.libretube.api.obj.Playlist
+import com.github.libretube.api.obj.PlaylistId
+import com.github.libretube.api.obj.Playlists
+import com.github.libretube.api.obj.SearchResult
+import com.github.libretube.api.obj.SegmentData
+import com.github.libretube.api.obj.StreamItem
+import com.github.libretube.api.obj.Streams
+import com.github.libretube.api.obj.Subscribe
+import com.github.libretube.api.obj.Subscription
+import com.github.libretube.api.obj.Token
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -8,81 +25,90 @@ import retrofit2.http.Path
import retrofit2.http.Query
interface PipedApi {
+ @GET("config")
+ suspend fun getConfig(): PipedConfig
+
@GET("trending")
- suspend fun getTrending(@Query("region") region: String): List
+ suspend fun getTrending(@Query("region") region: String): List
@GET("streams/{videoId}")
- suspend fun getStreams(@Path("videoId") videoId: String): com.github.libretube.api.obj.Streams
+ suspend fun getStreams(@Path("videoId") videoId: String): Streams
@GET("comments/{videoId}")
- suspend fun getComments(@Path("videoId") videoId: String): com.github.libretube.api.obj.CommentsPage
+ suspend fun getComments(@Path("videoId") videoId: String): CommentsPage
@GET("sponsors/{videoId}")
suspend fun getSegments(
@Path("videoId") videoId: String,
@Query("category") category: String
- ): com.github.libretube.api.obj.Segments
+ ): SegmentData
@GET("nextpage/comments/{videoId}")
suspend fun getCommentsNextPage(
@Path("videoId") videoId: String,
@Query("nextpage") nextPage: String
- ): com.github.libretube.api.obj.CommentsPage
+ ): CommentsPage
@GET("search")
suspend fun getSearchResults(
@Query("q") searchQuery: String,
@Query("filter") filter: String
- ): com.github.libretube.api.obj.SearchResult
+ ): SearchResult
@GET("nextpage/search")
suspend fun getSearchResultsNextPage(
@Query("q") searchQuery: String,
@Query("filter") filter: String,
@Query("nextpage") nextPage: String
- ): com.github.libretube.api.obj.SearchResult
+ ): SearchResult
@GET("suggestions")
suspend fun getSuggestions(@Query("query") query: String): List
@GET("channel/{channelId}")
- suspend fun getChannel(@Path("channelId") channelId: String): com.github.libretube.api.obj.Channel
+ suspend fun getChannel(@Path("channelId") channelId: String): Channel
+
+ @GET("channels/tabs")
+ suspend fun getChannelTab(
+ @Query("data") data: String,
+ @Query("nextpage") nextPage: String? = null
+ ): ChannelTabResponse
@GET("user/{name}")
- suspend fun getChannelByName(@Path("name") channelName: String): com.github.libretube.api.obj.Channel
+ suspend fun getChannelByName(@Path("name") channelName: String): Channel
@GET("nextpage/channel/{channelId}")
suspend fun getChannelNextPage(
@Path("channelId") channelId: String,
@Query("nextpage") nextPage: String
- ): com.github.libretube.api.obj.Channel
+ ): Channel
@GET("playlists/{playlistId}")
- suspend fun getPlaylist(@Path("playlistId") playlistId: String): com.github.libretube.api.obj.Playlist
+ suspend fun getPlaylist(@Path("playlistId") playlistId: String): Playlist
@GET("nextpage/playlists/{playlistId}")
suspend fun getPlaylistNextPage(
@Path("playlistId") playlistId: String,
@Query("nextpage") nextPage: String
- ): com.github.libretube.api.obj.Playlist
+ ): Playlist
@POST("login")
- suspend fun login(@Body login: com.github.libretube.api.obj.Login): com.github.libretube.api.obj.Token
+ suspend fun login(@Body login: Login): Token
@POST("register")
- suspend fun register(@Body login: com.github.libretube.api.obj.Login): com.github.libretube.api.obj.Token
+ suspend fun register(@Body login: Login): Token
@POST("user/delete")
suspend fun deleteAccount(
@Header("Authorization") token: String,
- @Body password: com.github.libretube.api.obj.DeleteUserRequest
+ @Body password: DeleteUserRequest
)
@GET("feed")
- suspend fun getFeed(@Query("authToken") token: String?): List
+ suspend fun getFeed(@Query("authToken") token: String?): List
@GET("feed/unauthenticated")
- suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List
+ suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List
@GET("subscribed")
suspend fun isSubscribed(
@@ -91,66 +117,66 @@ interface PipedApi {
): com.github.libretube.api.obj.Subscribed
@GET("subscriptions")
- suspend fun subscriptions(@Header("Authorization") token: String): List
+ suspend fun subscriptions(@Header("Authorization") token: String): List
@GET("subscriptions/unauthenticated")
- suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List
+ suspend fun unauthenticatedSubscriptions(@Query("channels") channels: String): List
@POST("subscribe")
suspend fun subscribe(
@Header("Authorization") token: String,
- @Body subscribe: com.github.libretube.api.obj.Subscribe
- ): com.github.libretube.api.obj.Message
+ @Body subscribe: Subscribe
+ ): Message
@POST("unsubscribe")
suspend fun unsubscribe(
@Header("Authorization") token: String,
- @Body subscribe: com.github.libretube.api.obj.Subscribe
- ): com.github.libretube.api.obj.Message
+ @Body subscribe: Subscribe
+ ): Message
@POST("import")
suspend fun importSubscriptions(
@Query("override") override: Boolean,
@Header("Authorization") token: String,
@Body channels: List
- ): com.github.libretube.api.obj.Message
+ ): Message
@POST("import/playlist")
suspend fun importPlaylist(
@Header("Authorization") token: String,
- @Body playlistId: com.github.libretube.api.obj.PlaylistId
- ): com.github.libretube.api.obj.Message
+ @Body playlistId: PlaylistId
+ ): PlaylistId
@GET("user/playlists")
- suspend fun playlists(@Header("Authorization") token: String): List
+ suspend fun getUserPlaylists(@Header("Authorization") token: String): List
@POST("user/playlists/rename")
suspend fun renamePlaylist(
@Header("Authorization") token: String,
- @Body playlistId: com.github.libretube.api.obj.PlaylistId
+ @Body playlistId: PlaylistId
)
@POST("user/playlists/delete")
suspend fun deletePlaylist(
@Header("Authorization") token: String,
- @Body playlistId: com.github.libretube.api.obj.PlaylistId
- ): com.github.libretube.api.obj.Message
+ @Body playlistId: PlaylistId
+ ): Message
@POST("user/playlists/create")
suspend fun createPlaylist(
@Header("Authorization") token: String,
- @Body name: com.github.libretube.api.obj.Playlists
- ): com.github.libretube.api.obj.PlaylistId
+ @Body name: Playlists
+ ): PlaylistId
@POST("user/playlists/add")
suspend fun addToPlaylist(
@Header("Authorization") token: String,
- @Body playlistId: com.github.libretube.api.obj.PlaylistId
- ): com.github.libretube.api.obj.Message
+ @Body playlistId: PlaylistId
+ ): Message
@POST("user/playlists/remove")
suspend fun removeFromPlaylist(
@Header("Authorization") token: String,
- @Body playlistId: com.github.libretube.api.obj.PlaylistId
- ): com.github.libretube.api.obj.Message
+ @Body playlistId: PlaylistId
+ ): Message
}
diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt
new file mode 100644
index 000000000..8b8beff9a
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt
@@ -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 {
+ if (loggedIn()) return RetrofitInstance.authApi.getUserPlaylists(token)
+
+ val localPlaylists = awaitQuery {
+ DatabaseHolder.Database.localPlaylistsDao().getAll()
+ }
+ val playlists = mutableListOf()
+ 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
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt
index 86c241d7f..a67dc3a2c 100644
--- a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt
+++ b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt
@@ -1,12 +1,18 @@
package com.github.libretube.api
+import android.content.Context
import android.util.Log
+import com.github.libretube.R
+import com.github.libretube.api.obj.StreamItem
+import com.github.libretube.api.obj.Subscription
+import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.util.PreferenceHelper
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -55,6 +61,24 @@ object SubscriptionHelper {
}
}
+ fun handleUnsubscribe(context: Context, channelId: String, channelName: String?, onUnsubscribe: () -> Unit) {
+ if (!PreferenceHelper.getBoolean(PreferenceKeys.CONFIRM_UNSUBSCRIBE, false)) {
+ unsubscribe(channelId)
+ onUnsubscribe.invoke()
+ return
+ }
+
+ MaterialAlertDialogBuilder(context)
+ .setTitle(R.string.unsubscribe)
+ .setMessage(context.getString(R.string.confirm_unsubscribe, channelName))
+ .setPositiveButton(R.string.unsubscribe) { _, _ ->
+ unsubscribe(channelId)
+ onUnsubscribe.invoke()
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ }
+
suspend fun isSubscribed(channelId: String): Boolean? {
if (PreferenceHelper.getToken() != "") {
val isSubscribed = try {
@@ -99,7 +123,7 @@ object SubscriptionHelper {
}
}
- fun getLocalSubscriptions(): List {
+ private fun getLocalSubscriptions(): List {
return awaitQuery {
Database.localSubscriptionDao().getAll()
}
@@ -107,6 +131,30 @@ object SubscriptionHelper {
fun getFormattedLocalSubscriptions(): String {
val localSubscriptions = getLocalSubscriptions()
- return localSubscriptions.map { it.channelId }.joinToString(",")
+ return localSubscriptions.joinToString(",") { it.channelId }
+ }
+
+ suspend fun getSubscriptions(): List {
+ return if (PreferenceHelper.getToken() != "") {
+ RetrofitInstance.authApi.subscriptions(
+ PreferenceHelper.getToken()
+ )
+ } else {
+ RetrofitInstance.authApi.unauthenticatedSubscriptions(
+ getFormattedLocalSubscriptions()
+ )
+ }
+ }
+
+ suspend fun getFeed(): List {
+ return if (PreferenceHelper.getToken() != "") {
+ RetrofitInstance.authApi.getFeed(
+ PreferenceHelper.getToken()
+ )
+ } else {
+ RetrofitInstance.authApi.getUnauthenticatedFeed(
+ getFormattedLocalSubscriptions()
+ )
+ }
}
}
diff --git a/app/src/main/java/com/github/libretube/api/obj/Channel.kt b/app/src/main/java/com/github/libretube/api/obj/Channel.kt
index d255187dd..e1b261df4 100644
--- a/app/src/main/java/com/github/libretube/api/obj/Channel.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/Channel.kt
@@ -12,5 +12,6 @@ data class Channel(
var nextpage: String? = null,
var subscriberCount: Long = 0,
var verified: Boolean = false,
- var relatedStreams: List? = null
+ var relatedStreams: List? = listOf(),
+ var tabs: List? = listOf()
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/Segments.kt b/app/src/main/java/com/github/libretube/api/obj/ChannelTab.kt
similarity index 64%
rename from app/src/main/java/com/github/libretube/api/obj/Segments.kt
rename to app/src/main/java/com/github/libretube/api/obj/ChannelTab.kt
index 704bd899c..c7e2d63de 100644
--- a/app/src/main/java/com/github/libretube/api/obj/Segments.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/ChannelTab.kt
@@ -3,6 +3,7 @@ package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
-data class Segments(
- val segments: MutableList = arrayListOf()
+data class ChannelTab(
+ val name: String? = null,
+ val data: String? = null
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt b/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt
new file mode 100644
index 000000000..6c6fd7bae
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt
@@ -0,0 +1,6 @@
+package com.github.libretube.api.obj
+
+data class ChannelTabResponse(
+ val content: List = listOf(),
+ val nextpage: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/api/obj/CommentsPage.kt b/app/src/main/java/com/github/libretube/api/obj/CommentsPage.kt
index 469c64a0c..cc0b4ad66 100644
--- a/app/src/main/java/com/github/libretube/api/obj/CommentsPage.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/CommentsPage.kt
@@ -6,5 +6,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
data class CommentsPage(
val comments: MutableList = arrayListOf(),
val disabled: Boolean? = null,
- val nextpage: String? = ""
+ val nextpage: String? = null
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/SearchItem.kt b/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt
similarity index 97%
rename from app/src/main/java/com/github/libretube/api/obj/SearchItem.kt
rename to app/src/main/java/com/github/libretube/api/obj/ContentItem.kt
index f55604d8b..af7181e5c 100644
--- a/app/src/main/java/com/github/libretube/api/obj/SearchItem.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt
@@ -3,7 +3,7 @@ package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
-data class SearchItem(
+data class ContentItem(
var url: String? = null,
var thumbnail: String? = null,
var uploaderName: String? = null,
diff --git a/app/src/main/java/com/github/libretube/api/obj/PipedConfig.kt b/app/src/main/java/com/github/libretube/api/obj/PipedConfig.kt
new file mode 100644
index 000000000..1595b674f
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/api/obj/PipedConfig.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt
index ed9e9d973..58de468e8 100644
--- a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt
@@ -17,5 +17,7 @@ data class PipedStream(
var indexEnd: Int? = null,
var width: Int? = null,
var height: Int? = null,
- var fps: Int? = null
+ var fps: Int? = null,
+ val audioTrackName: String? = null,
+ val audioTrackId: String? = null
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/Playlists.kt b/app/src/main/java/com/github/libretube/api/obj/Playlists.kt
index 2d46ced8f..9bee35d0e 100644
--- a/app/src/main/java/com/github/libretube/api/obj/Playlists.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/Playlists.kt
@@ -7,5 +7,6 @@ data class Playlists(
var id: String? = null,
var name: String? = null,
var shortDescription: String? = null,
- var thumbnail: String? = null
+ var thumbnail: String? = null,
+ var videos: Long? = null
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt b/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt
index 42ff66005..f923a6c7f 100644
--- a/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt
@@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class SearchResult(
- val items: MutableList? = arrayListOf(),
- val nextpage: String? = "",
+ val items: MutableList? = arrayListOf(),
+ val nextpage: String? = null,
val suggestion: String? = "",
val corrected: Boolean? = null
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/Segment.kt b/app/src/main/java/com/github/libretube/api/obj/Segment.kt
index ea92df057..951a7b472 100644
--- a/app/src/main/java/com/github/libretube/api/obj/Segment.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/Segment.kt
@@ -4,7 +4,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class Segment(
+ val UUID: String? = null,
val actionType: String? = null,
val category: String? = null,
- val segment: List? = arrayListOf()
+ val description: String? = null,
+ val locked: Int? = null,
+ val segment: List = listOf(),
+ val userID: String? = null,
+ val videoDuration: Double? = null,
+ val votes: Int? = null
)
diff --git a/app/src/main/java/com/github/libretube/api/obj/SegmentData.kt b/app/src/main/java/com/github/libretube/api/obj/SegmentData.kt
new file mode 100644
index 000000000..a383cd801
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/api/obj/SegmentData.kt
@@ -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 = listOf(),
+ val videoID: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/constants/Constants.kt b/app/src/main/java/com/github/libretube/constants/Constants.kt
index b3a959e13..4019f5d69 100644
--- a/app/src/main/java/com/github/libretube/constants/Constants.kt
+++ b/app/src/main/java/com/github/libretube/constants/Constants.kt
@@ -9,7 +9,6 @@ const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/re
* Links for the about fragment
*/
const val WEBSITE_URL = "https://libre-tube.github.io/"
-const val DONATE_URL = "https://github.com/libre-tube/LibreTube#donate"
const val GITHUB_URL = "https://github.com/libre-tube/LibreTube"
const val PIPED_GITHUB_URL = "https://github.com/TeamPiped/Piped"
const val WEBLATE_URL = "https://hosted.weblate.org/projects/libretube/libretube/"
@@ -27,7 +26,7 @@ const val TWITTER_URL = "https://twitter.com/libretube"
/**
* Share Dialog
*/
-const val PIPED_FRONTEND_URL = "https://piped.kavin.rocks"
+const val PIPED_FRONTEND_URL = "https://piped.video"
const val YOUTUBE_FRONTEND_URL = "https://www.youtube.com"
/**
diff --git a/app/src/main/java/com/github/libretube/constants/DownloadType.kt b/app/src/main/java/com/github/libretube/constants/DownloadType.kt
deleted file mode 100644
index 702c140b3..000000000
--- a/app/src/main/java/com/github/libretube/constants/DownloadType.kt
+++ /dev/null
@@ -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
-}
diff --git a/app/src/main/java/com/github/libretube/constants/IntentData.kt b/app/src/main/java/com/github/libretube/constants/IntentData.kt
index 82bef9e0b..b1f1d6a26 100644
--- a/app/src/main/java/com/github/libretube/constants/IntentData.kt
+++ b/app/src/main/java/com/github/libretube/constants/IntentData.kt
@@ -8,4 +8,6 @@ object IntentData {
const val timeStamp = "timeStamp"
const val position = "position"
const val fileName = "fileName"
+ const val openQueueOnce = "openQueue"
+ const val playlistType = "playlistType"
}
diff --git a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt
index 1fdd5e0ce..e87a00604 100644
--- a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt
+++ b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt
@@ -10,6 +10,7 @@ object PreferenceKeys {
const val AUTH_PREF_FILE = "auth"
const val TOKEN = "token"
const val USERNAME = "username"
+ const val IMAGE_PROXY_URL = "image_proxy_url"
/**
* General
@@ -20,7 +21,7 @@ object PreferenceKeys {
const val BREAK_REMINDER_TOGGLE = "break_reminder_toggle"
const val BREAK_REMINDER = "break_reminder"
const val SAVE_FEED = "save_feed"
- const val NAVBAR_ITEMS = "nav_bar_items"
+ const val NAVBAR_ITEMS = "navbar_items"
/**
* Appearance
@@ -33,7 +34,7 @@ object PreferenceKeys {
const val APP_ICON = "icon_change"
const val LEGACY_SUBSCRIPTIONS = "legacy_subscriptions"
const val LEGACY_SUBSCRIPTIONS_COLUMNS = "legacy_subscriptions_columns"
- const val ALTERNATIVE_TRENDING_LAYOUT = "trending_layout"
+ const val ALTERNATIVE_VIDEOS_LAYOUT = "alternative_videos_layout"
const val NEW_VIDEOS_BADGE = "new_videos_badge"
const val PLAYLISTS_ORDER = "playlists_order"
@@ -77,13 +78,16 @@ object PreferenceKeys {
const val PICTURE_IN_PICTURE = "picture_in_picture"
const val PLAYER_RESIZE_MODE = "player_resize_mode"
const val SB_SKIP_MANUALLY = "sb_skip_manually_key"
- const val LIMIT_HLS = "limit_hls"
- const val PROGRESSIVE_LOADING_INTERVAL_SIZE = "progressive_loading_interval"
+ const val SB_SHOW_MARKERS = "sb_show_markers"
+ const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout"
+ const val USE_HLS_OVER_DASH = "use_hls"
+ const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos"
/**
* Background mode
*/
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
+ const val NOTIFICATION_OPEN_QUEUE = "notification_open_queue"
/**
* Notifications
@@ -92,6 +96,10 @@ object PreferenceKeys {
const val CHECKING_FREQUENCY = "checking_frequency"
const val REQUIRED_NETWORK = "required_network"
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
+ const val IGNORED_NOTIFICATION_CHANNELS = "ignored_notification_channels"
+ const val NOTIFICATION_TIME_ENABLED = "notification_time"
+ const val NOTIFICATION_START_TIME = "notification_start_time"
+ const val NOTIFICATION_END_TIME = "notification_end_time"
/**
* Advanced
@@ -103,6 +111,8 @@ object PreferenceKeys {
const val CLEAR_WATCH_HISTORY = "clear_watch_history"
const val CLEAR_WATCH_POSITIONS = "clear_watch_positions"
const val SHARE_WITH_TIME_CODE = "share_with_time_code"
+ const val CONFIRM_UNSUBSCRIBE = "confirm_unsubscribing"
+ const val CLEAR_BOOKMARKS = "clear_bookmarks"
/**
* History
diff --git a/app/src/main/java/com/github/libretube/constants/ShareObjectType.kt b/app/src/main/java/com/github/libretube/constants/ShareObjectType.kt
deleted file mode 100644
index 175585f65..000000000
--- a/app/src/main/java/com/github/libretube/constants/ShareObjectType.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.libretube.constants
-
-object ShareObjectType {
- const val VIDEO = 0
- const val PLAYLIST = 1
- const val CHANNEL = 2
-}
diff --git a/app/src/main/java/com/github/libretube/db/AppDatabase.kt b/app/src/main/java/com/github/libretube/db/AppDatabase.kt
index 88daecd16..9342e51b4 100644
--- a/app/src/main/java/com/github/libretube/db/AppDatabase.kt
+++ b/app/src/main/java/com/github/libretube/db/AppDatabase.kt
@@ -1,14 +1,20 @@
package com.github.libretube.db
+import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.libretube.db.dao.CustomInstanceDao
+import com.github.libretube.db.dao.LocalPlaylistsDao
import com.github.libretube.db.dao.LocalSubscriptionDao
+import com.github.libretube.db.dao.PlaylistBookmarkDao
import com.github.libretube.db.dao.SearchHistoryDao
import com.github.libretube.db.dao.WatchHistoryDao
import com.github.libretube.db.dao.WatchPositionDao
import com.github.libretube.db.obj.CustomInstance
+import com.github.libretube.db.obj.LocalPlaylist
+import com.github.libretube.db.obj.LocalPlaylistItem
import com.github.libretube.db.obj.LocalSubscription
+import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition
@@ -19,9 +25,16 @@ import com.github.libretube.db.obj.WatchPosition
WatchPosition::class,
SearchHistoryItem::class,
CustomInstance::class,
- LocalSubscription::class
+ LocalSubscription::class,
+ PlaylistBookmark::class,
+ LocalPlaylist::class,
+ LocalPlaylistItem::class
],
- version = 7
+ version = 9,
+ autoMigrations = [
+ AutoMigration(from = 7, to = 8),
+ AutoMigration(from = 8, to = 9)
+ ]
)
abstract class AppDatabase : RoomDatabase() {
/**
@@ -48,4 +61,14 @@ abstract class AppDatabase : RoomDatabase() {
* Local Subscriptions
*/
abstract fun localSubscriptionDao(): LocalSubscriptionDao
+
+ /**
+ * Bookmarked Playlists
+ */
+ abstract fun playlistBookmarkDao(): PlaylistBookmarkDao
+
+ /**
+ * Local playlists
+ */
+ abstract fun localPlaylistsDao(): LocalPlaylistsDao
}
diff --git a/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt b/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt
index c5c2a21c8..c6fdf259f 100644
--- a/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt
+++ b/app/src/main/java/com/github/libretube/db/DatabaseHelper.kt
@@ -5,12 +5,13 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
-import com.github.libretube.db.obj.WatchPosition
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.util.PreferenceHelper
object DatabaseHelper {
+ private const val MAX_SEARCH_HISTORY_SIZE = 20
+
fun addToWatchHistory(videoId: String, streams: Streams) {
val watchHistoryItem = WatchHistoryItem(
videoId,
@@ -37,40 +38,13 @@ object DatabaseHelper {
}
}
- fun removeFromWatchHistory(index: Int) {
- query {
- Database.watchHistoryDao().delete(
- Database.watchHistoryDao().getAll()[index]
- )
- }
- }
-
- fun saveWatchPosition(videoId: String, position: Long) {
- val watchPosition = WatchPosition(
- videoId,
- position
- )
- query {
- Database.watchPositionDao().insertAll(watchPosition)
- }
- }
-
- fun removeWatchPosition(videoId: String) {
- query {
- Database.watchPositionDao().findById(videoId)?.let {
- Database.watchPositionDao().delete(it)
- }
- }
- }
-
fun addToSearchHistory(searchHistoryItem: SearchHistoryItem) {
query {
Database.searchHistoryDao().insertAll(searchHistoryItem)
- val maxHistorySize = 20
// delete the first watch history entry if the limit is reached
val searchHistory = Database.searchHistoryDao().getAll()
- if (searchHistory.size > maxHistorySize) {
+ if (searchHistory.size > MAX_SEARCH_HISTORY_SIZE) {
Database.searchHistoryDao()
.delete(searchHistory.first())
}
diff --git a/app/src/main/java/com/github/libretube/db/dao/LocalPlaylistsDao.kt b/app/src/main/java/com/github/libretube/db/dao/LocalPlaylistsDao.kt
new file mode 100644
index 000000000..f0eb47950
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/db/dao/LocalPlaylistsDao.kt
@@ -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
+
+ @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)
+}
diff --git a/app/src/main/java/com/github/libretube/db/dao/PlaylistBookmarkDao.kt b/app/src/main/java/com/github/libretube/db/dao/PlaylistBookmarkDao.kt
new file mode 100644
index 000000000..493c96470
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/db/dao/PlaylistBookmarkDao.kt
@@ -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
+
+ @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()
+}
diff --git a/app/src/main/java/com/github/libretube/db/dao/WatchPositionDao.kt b/app/src/main/java/com/github/libretube/db/dao/WatchPositionDao.kt
index 6855eaab4..b08f6e2cf 100644
--- a/app/src/main/java/com/github/libretube/db/dao/WatchPositionDao.kt
+++ b/app/src/main/java/com/github/libretube/db/dao/WatchPositionDao.kt
@@ -21,6 +21,9 @@ interface WatchPositionDao {
@Delete
fun delete(watchPosition: WatchPosition)
+ @Query("DELETE FROM watchHistoryItem WHERE videoId = :videoId")
+ fun deleteById(videoId: String)
+
@Query("DELETE FROM watchPosition")
fun deleteAll()
}
diff --git a/app/src/main/java/com/github/libretube/db/obj/LocalPlaylist.kt b/app/src/main/java/com/github/libretube/db/obj/LocalPlaylist.kt
new file mode 100644
index 000000000..2364af951
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/db/obj/LocalPlaylist.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/libretube/db/obj/LocalPlaylistItem.kt b/app/src/main/java/com/github/libretube/db/obj/LocalPlaylistItem.kt
new file mode 100644
index 000000000..65d9dc134
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/db/obj/LocalPlaylistItem.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/libretube/db/obj/LocalPlaylistWithVideos.kt b/app/src/main/java/com/github/libretube/db/obj/LocalPlaylistWithVideos.kt
new file mode 100644
index 000000000..a4cca23cd
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/db/obj/LocalPlaylistWithVideos.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/libretube/db/obj/PlaylistBookmark.kt b/app/src/main/java/com/github/libretube/db/obj/PlaylistBookmark.kt
new file mode 100644
index 000000000..fa7280580
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/db/obj/PlaylistBookmark.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt b/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt
index c5fed1b1b..45da337df 100644
--- a/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt
+++ b/app/src/main/java/com/github/libretube/db/obj/WatchHistoryItem.kt
@@ -11,7 +11,7 @@ data class WatchHistoryItem(
@ColumnInfo val uploadDate: String? = null,
@ColumnInfo val uploader: String? = null,
@ColumnInfo val uploaderUrl: String? = null,
- @ColumnInfo val uploaderAvatar: String? = null,
- @ColumnInfo val thumbnailUrl: String? = null,
+ @ColumnInfo var uploaderAvatar: String? = null,
+ @ColumnInfo var thumbnailUrl: String? = null,
@ColumnInfo val duration: Long? = null
)
diff --git a/app/src/main/java/com/github/libretube/enums/DownloadType.kt b/app/src/main/java/com/github/libretube/enums/DownloadType.kt
new file mode 100644
index 000000000..0df017b47
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/enums/DownloadType.kt
@@ -0,0 +1,11 @@
+package com.github.libretube.enums
+
+/**
+ * object for saving the download type
+ */
+enum class DownloadType {
+ AUDIO,
+ VIDEO,
+ AUDIO_VIDEO,
+ NONE
+}
diff --git a/app/src/main/java/com/github/libretube/enums/PlaylistType.kt b/app/src/main/java/com/github/libretube/enums/PlaylistType.kt
new file mode 100644
index 000000000..852a1cda9
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/enums/PlaylistType.kt
@@ -0,0 +1,18 @@
+package com.github.libretube.enums
+
+enum class PlaylistType {
+ /**
+ * Local playlist
+ */
+ LOCAL,
+
+ /**
+ * Piped playlist
+ */
+ PRIVATE,
+
+ /**
+ * YouTube playlist
+ */
+ PUBLIC
+}
diff --git a/app/src/main/java/com/github/libretube/enums/ShareObjectType.kt b/app/src/main/java/com/github/libretube/enums/ShareObjectType.kt
new file mode 100644
index 000000000..09b55be67
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/enums/ShareObjectType.kt
@@ -0,0 +1,7 @@
+package com.github.libretube.enums
+
+enum class ShareObjectType {
+ VIDEO,
+ PLAYLIST,
+ CHANNEL
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/CreateDir.kt b/app/src/main/java/com/github/libretube/extensions/CreateDir.kt
deleted file mode 100644
index 5300ff318..000000000
--- a/app/src/main/java/com/github/libretube/extensions/CreateDir.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.libretube.extensions
-
-import java.io.File
-
-fun File.createDir() = apply {
- if (!this.exists()) this.mkdirs()
-}
diff --git a/app/src/main/java/com/github/libretube/extensions/FormatShort.kt b/app/src/main/java/com/github/libretube/extensions/FormatShort.kt
index f0751db56..bb010765b 100644
--- a/app/src/main/java/com/github/libretube/extensions/FormatShort.kt
+++ b/app/src/main/java/com/github/libretube/extensions/FormatShort.kt
@@ -3,15 +3,17 @@ package com.github.libretube.extensions
import java.math.BigDecimal
import java.math.RoundingMode
+@Suppress("KotlinConstantConditions")
fun Long?.formatShort(): String = when {
- this!! < 1000 -> {
+ this == null -> (0).toString()
+ this < 1000 -> {
this.toString()
}
- this in 1000..999999 -> {
+ this in (1000..999999) -> {
val decimal = BigDecimal(this / 1000).setScale(0, RoundingMode.HALF_EVEN)
decimal.toString() + "K"
}
- this in 1000000..10000000 -> {
+ this in (1000000..10000000) -> {
val decimal = BigDecimal(this / 1000000).setScale(0, RoundingMode.HALF_EVEN)
decimal.toString() + "M"
}
diff --git a/app/src/main/java/com/github/libretube/extensions/Move.kt b/app/src/main/java/com/github/libretube/extensions/Move.kt
new file mode 100644
index 000000000..bd556be87
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/Move.kt
@@ -0,0 +1,7 @@
+package com.github.libretube.extensions
+
+fun MutableList.move(oldPosition: Int, newPosition: Int) {
+ val item = this.get(oldPosition)
+ this.removeAt(oldPosition)
+ this.add(newPosition, item)
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/Round.kt b/app/src/main/java/com/github/libretube/extensions/Round.kt
new file mode 100644
index 000000000..0be63d173
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/Round.kt
@@ -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()
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/Serializable.kt b/app/src/main/java/com/github/libretube/extensions/Serializable.kt
new file mode 100644
index 000000000..25c9cb521
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/Serializable.kt
@@ -0,0 +1,13 @@
+package com.github.libretube.ui.extensions
+
+import android.os.Build
+import android.os.Bundle
+import java.io.Serializable
+
+inline fun 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
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/TAG.kt b/app/src/main/java/com/github/libretube/extensions/Tag.kt
similarity index 100%
rename from app/src/main/java/com/github/libretube/extensions/TAG.kt
rename to app/src/main/java/com/github/libretube/extensions/Tag.kt
diff --git a/app/src/main/java/com/github/libretube/extensions/ToID.kt b/app/src/main/java/com/github/libretube/extensions/ToID.kt
index df7b284e0..b3439cb72 100644
--- a/app/src/main/java/com/github/libretube/extensions/ToID.kt
+++ b/app/src/main/java/com/github/libretube/extensions/ToID.kt
@@ -3,9 +3,8 @@ package com.github.libretube.extensions
/**
* format a Piped route to an ID
*/
-fun Any.toID(): String {
+fun String.toID(): String {
return this
- .toString()
.replace("/watch?v=", "") // videos
.replace("/channel/", "") // channels
.replace("/playlist?list=", "") // playlists
diff --git a/app/src/main/java/com/github/libretube/extensions/ToLocalPlaylistItem.kt b/app/src/main/java/com/github/libretube/extensions/ToLocalPlaylistItem.kt
new file mode 100644
index 000000000..11146b1f6
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/ToLocalPlaylistItem.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/ToStreamItem.kt b/app/src/main/java/com/github/libretube/extensions/ToStreamItem.kt
new file mode 100644
index 000000000..2be7bbde4
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/ToStreamItem.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt b/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt
new file mode 100644
index 000000000..2be369899
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt
@@ -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))
+}
diff --git a/app/src/main/java/com/github/libretube/models/interfaces/DoubleTapInterface.kt b/app/src/main/java/com/github/libretube/models/interfaces/DoubleTapInterface.kt
deleted file mode 100644
index b26a13b4f..000000000
--- a/app/src/main/java/com/github/libretube/models/interfaces/DoubleTapInterface.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.github.libretube.models.interfaces
-
-interface DoubleTapInterface {
- fun onEvent(x: Float)
-}
diff --git a/app/src/main/java/com/github/libretube/models/interfaces/PlayerOptionsInterface.kt b/app/src/main/java/com/github/libretube/models/interfaces/PlayerOptionsInterface.kt
deleted file mode 100644
index 66d3d87d9..000000000
--- a/app/src/main/java/com/github/libretube/models/interfaces/PlayerOptionsInterface.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.libretube.models.interfaces
-
-interface PlayerOptionsInterface {
- fun onCaptionClicked()
-
- fun onQualityClicked()
-}
diff --git a/app/src/main/java/com/github/libretube/obj/BackupFile.kt b/app/src/main/java/com/github/libretube/obj/BackupFile.kt
index 5d6770956..82e8a75ec 100644
--- a/app/src/main/java/com/github/libretube/obj/BackupFile.kt
+++ b/app/src/main/java/com/github/libretube/obj/BackupFile.kt
@@ -1,7 +1,9 @@
package com.github.libretube.obj
import com.github.libretube.db.obj.CustomInstance
+import com.github.libretube.db.obj.LocalPlaylistWithVideos
import com.github.libretube.db.obj.LocalSubscription
+import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition
@@ -12,5 +14,7 @@ data class BackupFile(
var searchHistory: List? = null,
var localSubscriptions: List? = null,
var customInstances: List? = null,
+ var playlistBookmarks: List? = null,
+ var localPlaylists: List? = null,
var preferences: List? = null
)
diff --git a/app/src/main/java/com/github/libretube/obj/BottomSheetItem.kt b/app/src/main/java/com/github/libretube/obj/BottomSheetItem.kt
index d9614e5f8..73195412a 100644
--- a/app/src/main/java/com/github/libretube/obj/BottomSheetItem.kt
+++ b/app/src/main/java/com/github/libretube/obj/BottomSheetItem.kt
@@ -3,5 +3,6 @@ package com.github.libretube.obj
data class BottomSheetItem(
val title: String,
val drawable: Int? = null,
- val currentValue: String? = null
+ val getCurrent: () -> String? = { null },
+ val onClick: () -> Unit = {}
)
diff --git a/app/src/main/java/com/github/libretube/obj/ChannelTabs.kt b/app/src/main/java/com/github/libretube/obj/ChannelTabs.kt
new file mode 100644
index 000000000..9a7c91857
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/obj/ChannelTabs.kt
@@ -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)
+}
diff --git a/app/src/main/java/com/github/libretube/obj/DownloadedFile.kt b/app/src/main/java/com/github/libretube/obj/DownloadedFile.kt
index ffa9c9d55..83450823d 100644
--- a/app/src/main/java/com/github/libretube/obj/DownloadedFile.kt
+++ b/app/src/main/java/com/github/libretube/obj/DownloadedFile.kt
@@ -6,7 +6,6 @@ import com.github.libretube.api.obj.Streams
data class DownloadedFile(
val name: String,
val size: Long,
- val type: Int,
var metadata: Streams? = null,
var thumbnail: Bitmap? = null
)
diff --git a/app/src/main/java/com/github/libretube/obj/NavBarItem.kt b/app/src/main/java/com/github/libretube/obj/NavBarItem.kt
deleted file mode 100644
index eac14a1c7..000000000
--- a/app/src/main/java/com/github/libretube/obj/NavBarItem.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.github.libretube.obj
-
-data class NavBarItem(
- val id: Int = 0,
- val title: String = "",
- var isEnabled: Boolean = true
-)
diff --git a/app/src/main/java/com/github/libretube/obj/ShareData.kt b/app/src/main/java/com/github/libretube/obj/ShareData.kt
new file mode 100644
index 000000000..532558af0
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/obj/ShareData.kt
@@ -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
+)
diff --git a/app/src/main/java/com/github/libretube/obj/VideoResolution.kt b/app/src/main/java/com/github/libretube/obj/VideoResolution.kt
new file mode 100644
index 000000000..b234cfda2
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/obj/VideoResolution.kt
@@ -0,0 +1,7 @@
+package com.github.libretube.obj
+
+data class VideoResolution(
+ val name: String,
+ val resolution: Int? = null,
+ val adaptiveSourceUrl: String? = null
+)
diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt
index 226643432..719d523cd 100644
--- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt
+++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt
@@ -9,23 +9,24 @@ import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
-import android.util.Log
import android.widget.Toast
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Segment
-import com.github.libretube.api.obj.Segments
+import com.github.libretube.api.obj.SegmentData
import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
import com.github.libretube.constants.PreferenceKeys
-import com.github.libretube.db.DatabaseHelper
-import com.github.libretube.db.DatabaseHolder
+import com.github.libretube.db.DatabaseHolder.Companion.Database
+import com.github.libretube.db.obj.WatchPosition
+import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.awaitQuery
+import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
-import com.github.libretube.util.AutoPlayHelper
+import com.github.libretube.extensions.toStreamItem
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayingQueue
@@ -53,6 +54,7 @@ class BackgroundMode : Service() {
*PlaylistId for autoplay
*/
private var playlistId: String? = null
+ private var playlistType: PlaylistType? = null
/**
* The response that gets when called the Api.
@@ -73,23 +75,13 @@ class BackgroundMode : Service() {
/**
* SponsorBlock Segment data
*/
- private var segmentData: Segments? = null
+ private var segmentData: SegmentData? = null
/**
* [Notification] for the player
*/
private lateinit var nowPlayingNotification: NowPlayingNotification
- /**
- * The [videoId] of the next stream for autoplay
- */
- private var nextStreamId: String? = null
-
- /**
- * Helper for finding the next video in the playlist
- */
- private lateinit var autoPlayHelper: AutoPlayHelper
-
/**
* Autoplay Preference
*/
@@ -100,18 +92,21 @@ class BackgroundMode : Service() {
*/
override fun onCreate() {
super.onCreate()
- if (Build.VERSION.SDK_INT >= 26) {
- val channelId = BACKGROUND_CHANNEL_ID
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
- channelId,
+ BACKGROUND_CHANNEL_ID,
"Background Service",
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
- val notification: Notification = Notification.Builder(this, channelId)
+
+ // see https://developer.android.com/reference/android/app/Service#startForeground(int,%20android.app.Notification)
+ val notification: Notification = Notification.Builder(this, BACKGROUND_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
- .setContentText(getString(R.string.playingOnBackground)).build()
+ .setContentText(getString(R.string.playingOnBackground))
+ .build()
+
startForeground(PLAYER_NOTIFICATION_ID, notification)
}
}
@@ -122,20 +117,21 @@ class BackgroundMode : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
// clear the playing queue
- PlayingQueue.clear()
+ PlayingQueue.resetToDefaults()
// get the intent arguments
videoId = intent?.getStringExtra(IntentData.videoId)!!
playlistId = intent.getStringExtra(IntentData.playlistId)
val position = intent.getLongExtra(IntentData.position, 0L)
- // initialize the playlist autoPlay Helper
- autoPlayHelper = AutoPlayHelper(playlistId)
-
// play the audio in the background
loadAudio(videoId, position)
- updateWatchPosition()
+ PlayingQueue.setOnQueueTapListener { streamItem ->
+ streamItem.url?.toID()?.let { playNextVideo(it) }
+ }
+
+ if (PlayerHelper.watchPositionsEnabled) updateWatchPosition()
} catch (e: Exception) {
onDestroy()
}
@@ -143,7 +139,11 @@ class BackgroundMode : Service() {
}
private fun updateWatchPosition() {
- player?.currentPosition?.let { DatabaseHelper.saveWatchPosition(videoId, it) }
+ player?.currentPosition?.let {
+ query {
+ Database.watchPositionDao().insertAll(WatchPosition(videoId, it))
+ }
+ }
handler.postDelayed(this::updateWatchPosition, 500)
}
@@ -154,8 +154,6 @@ class BackgroundMode : Service() {
videoId: String,
seekToPosition: Long = 0
) {
- // append the video to the playing queue
- PlayingQueue.add(videoId)
CoroutineScope(Dispatchers.IO).launch {
try {
streams = RetrofitInstance.api.getStreams(videoId)
@@ -163,6 +161,21 @@ class BackgroundMode : Service() {
return@launch
}
+ // add the playlist video to the queue
+ if (playlistId != null && PlayingQueue.isEmpty()) {
+ streams?.toStreamItem(videoId)
+ ?.let {
+ PlayingQueue.insertPlaylist(playlistId!!, it)
+ }
+ } else {
+ streams?.toStreamItem(videoId)?.let {
+ PlayingQueue.updateCurrent(it)
+ }
+ streams?.relatedStreams?.toTypedArray()?.let {
+ if (PlayerHelper.autoInsertRelatedVideos) PlayingQueue.add(*it)
+ }
+ }
+
handler.post {
playAudio(seekToPosition)
}
@@ -172,8 +185,6 @@ class BackgroundMode : Service() {
private fun playAudio(
seekToPosition: Long
) {
- PlayingQueue.updateCurrent(videoId)
-
initializePlayer()
setMediaItem()
@@ -194,9 +205,8 @@ class BackgroundMode : Service() {
} else if (PlayerHelper.watchPositionsEnabled) {
try {
val watchPosition = awaitQuery {
- DatabaseHolder.Database.watchPositionDao().findById(videoId)
+ Database.watchPositionDao().findById(videoId)
}
- Log.e("position", watchPosition.toString())
streams?.duration?.let {
if (watchPosition != null && watchPosition.position < it * 1000 * 0.9) {
player?.seekTo(watchPosition.position)
@@ -215,8 +225,6 @@ class BackgroundMode : Service() {
player?.setPlaybackSpeed(playbackSpeed)
fetchSponsorBlockSegments()
-
- if (PlayerHelper.autoPlayEnabled) setNextStream()
}
/**
@@ -265,32 +273,16 @@ class BackgroundMode : Service() {
})
}
- /**
- * set the videoId of the next stream for autoplay
- */
- private fun setNextStream() {
- if (streams!!.relatedStreams!!.isNotEmpty()) {
- nextStreamId = streams?.relatedStreams!![0].url!!.toID()
- }
-
- if (playlistId == null) return
- if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
- // search for the next videoId in the playlist
- CoroutineScope(Dispatchers.IO).launch {
- nextStreamId = autoPlayHelper.getNextVideoId(videoId, streams!!.relatedStreams!!)
- }
- }
-
/**
* Plays the first related video to the current (used when the playback of the current video ended)
*/
- private fun playNextVideo() {
- if (nextStreamId == null || nextStreamId == videoId) return
- val nextQueueVideo = PlayingQueue.getNext()
- if (nextQueueVideo != null) nextStreamId = nextQueueVideo
+ private fun playNextVideo(nextId: String? = null) {
+ val nextVideo = nextId ?: PlayingQueue.getNext()
// play new video on background
- this.videoId = nextStreamId!!
+ if (nextVideo != null) {
+ this.videoId = nextVideo
+ }
this.segmentData = null
loadAudio(videoId)
}
@@ -322,16 +314,15 @@ class BackgroundMode : Service() {
*/
private fun fetchSponsorBlockSegments() {
CoroutineScope(Dispatchers.IO).launch {
- kotlin.runCatching {
+ runCatching {
val categories = PlayerHelper.getSponsorBlockCategories()
- if (categories.size > 0) {
- segmentData =
- RetrofitInstance.api.getSegments(
- videoId,
- ObjectMapper().writeValueAsString(categories)
- )
- checkForSegments()
- }
+ if (categories.isEmpty()) return@runCatching
+ segmentData =
+ RetrofitInstance.api.getSegments(
+ videoId,
+ ObjectMapper().writeValueAsString(categories)
+ )
+ checkForSegments()
}
}
}
@@ -345,7 +336,7 @@ class BackgroundMode : Service() {
if (segmentData == null || segmentData!!.segments.isEmpty()) return
segmentData!!.segments.forEach { segment: Segment ->
- val segmentStart = (segment.segment!![0] * 1000f).toLong()
+ val segmentStart = (segment.segment[0] * 1000f).toLong()
val segmentEnd = (segment.segment[1] * 1000f).toLong()
val currentPosition = player?.currentPosition
if (currentPosition in segmentStart until segmentEnd) {
@@ -367,7 +358,7 @@ class BackgroundMode : Service() {
*/
override fun onDestroy() {
// clear the playing queue
- PlayingQueue.clear()
+ PlayingQueue.resetToDefaults()
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroySelfAndPlayer()
diff --git a/app/src/main/java/com/github/libretube/services/DownloadService.kt b/app/src/main/java/com/github/libretube/services/DownloadService.kt
index dc5893993..ede6e3667 100644
--- a/app/src/main/java/com/github/libretube/services/DownloadService.kt
+++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt
@@ -15,7 +15,7 @@ import com.github.libretube.R
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
import com.github.libretube.constants.DOWNLOAD_FAILURE_NOTIFICATION_ID
import com.github.libretube.constants.DOWNLOAD_SUCCESS_NOTIFICATION_ID
-import com.github.libretube.constants.DownloadType
+import com.github.libretube.enums.DownloadType
import com.github.libretube.extensions.TAG
import com.github.libretube.util.DownloadHelper
import java.io.File
@@ -25,7 +25,7 @@ class DownloadService : Service() {
private lateinit var videoName: String
private lateinit var videoUrl: String
private lateinit var audioUrl: String
- private var downloadType: Int = 3
+ private var downloadType: DownloadType = DownloadType.NONE
private var videoDownloadId: Long? = null
private var audioDownloadId: Long? = null
@@ -63,8 +63,8 @@ class DownloadService : Service() {
private fun downloadManager() {
// initialize and create the directories to download into
- val videoDownloadDir = DownloadHelper.getVideoDir(this)
- val audioDownloadDir = DownloadHelper.getAudioDir(this)
+ val videoDownloadDir = DownloadHelper.getDownloadDir(this, DownloadHelper.VIDEO_DIR)
+ val audioDownloadDir = DownloadHelper.getDownloadDir(this, DownloadHelper.AUDIO_DIR)
// start download
try {
@@ -74,7 +74,7 @@ class DownloadService : Service() {
)
if (downloadType in listOf(DownloadType.VIDEO, DownloadType.AUDIO_VIDEO)) {
videoDownloadId = downloadManagerRequest(
- getString(R.string.video),
+ "[${getString(R.string.video)}] $videoName",
getString(R.string.downloading),
videoUrl,
Uri.fromFile(
@@ -84,7 +84,7 @@ class DownloadService : Service() {
}
if (downloadType in listOf(DownloadType.AUDIO, DownloadType.AUDIO_VIDEO)) {
audioDownloadId = downloadManagerRequest(
- getString(R.string.audio),
+ "[${getString(R.string.audio)}] $videoName",
getString(R.string.downloading),
audioUrl,
Uri.fromFile(
diff --git a/app/src/main/java/com/github/libretube/services/UpdateService.kt b/app/src/main/java/com/github/libretube/services/UpdateService.kt
index 8b3e566de..cc25c9e43 100644
--- a/app/src/main/java/com/github/libretube/services/UpdateService.kt
+++ b/app/src/main/java/com/github/libretube/services/UpdateService.kt
@@ -11,6 +11,7 @@ import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import com.github.libretube.R
+import com.github.libretube.util.DownloadHelper
import java.io.File
class UpdateService : Service() {
@@ -28,9 +29,7 @@ class UpdateService : Service() {
}
private fun downloadApk(downloadUrl: String) {
- val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- // val dir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
- file = File(dir, "release.apk")
+ file = File(getDownloadDirectory(), "release.apk")
val request: DownloadManager.Request =
DownloadManager.Request(Uri.parse(downloadUrl))
@@ -80,6 +79,12 @@ class UpdateService : Service() {
}
}
+ private fun getDownloadDirectory(): File {
+ val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ if (!downloadsDir.canWrite()) return DownloadHelper.getOfflineStorageDir(this)
+ return downloadsDir
+ }
+
override fun onDestroy() {
unregisterReceiver(onDownloadComplete)
super.onDestroy()
diff --git a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt
index 32286d1dd..08a00e62a 100644
--- a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt
+++ b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt
@@ -14,10 +14,10 @@ import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
-import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
+import androidx.core.view.children
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
@@ -27,18 +27,20 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.extensions.toID
-import com.github.libretube.models.PlayerViewModel
-import com.github.libretube.models.SearchViewModel
-import com.github.libretube.models.SubscriptionsViewModel
import com.github.libretube.services.ClosingService
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.ErrorDialog
import com.github.libretube.ui.fragments.PlayerFragment
+import com.github.libretube.ui.models.PlayerViewModel
+import com.github.libretube.ui.models.SearchViewModel
+import com.github.libretube.ui.models.SubscriptionsViewModel
+import com.github.libretube.ui.sheets.PlayingQueueSheet
+import com.github.libretube.ui.tools.BreakReminder
import com.github.libretube.util.NavBarHelper
import com.github.libretube.util.NetworkHelper
+import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
class MainActivity : BaseActivity() {
@@ -47,21 +49,17 @@ class MainActivity : BaseActivity() {
lateinit var navController: NavController
private var startFragmentId = R.id.homeFragment
- var autoRotationEnabled = false
+
+ val autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
lateinit var searchView: SearchView
+ lateinit var searchItem: MenuItem
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- autoRotationEnabled = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_ROTATION, false)
-
// enable auto rotation if turned on
- requestedOrientation = if (autoRotationEnabled) {
- ActivityInfo.SCREEN_ORIENTATION_USER
- } else {
- ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
- }
+ requestOrientationChange()
// start service that gets called on closure
try {
@@ -93,8 +91,12 @@ class MainActivity : BaseActivity() {
// sets the navigation bar color to the previously calculated color
window.navigationBarColor = color
- // save start tab fragment id
- startFragmentId = NavBarHelper.applyNavBarStyle(binding.bottomNav)
+ // save start tab fragment id and apply navbar style
+ startFragmentId = try {
+ NavBarHelper.applyNavBarStyle(binding.bottomNav)
+ } catch (e: Exception) {
+ R.id.homeFragment
+ }
// set default tab as start fragment
navController.graph.setStartDestination(startFragmentId)
@@ -104,18 +106,16 @@ class MainActivity : BaseActivity() {
binding.bottomNav.setOnApplyWindowInsetsListener(null)
- binding.bottomNav.setOnItemSelectedListener {
- // clear backstack if it's the start fragment
- if (startFragmentId == it.itemId) navController.backQueue.clear()
-
- if (it.itemId == R.id.subscriptionsFragment) {
- binding.bottomNav.removeBadge(R.id.subscriptionsFragment)
+ // Prevent duplicate entries into backstack, if selected item and current
+ // visible fragment is different, then navigate to selected item.
+ binding.bottomNav.setOnItemReselectedListener {
+ if (it.itemId != navController.currentDestination?.id) {
+ navigateToBottomSelectedItem(it)
}
+ }
- removeSearchFocus()
-
- // navigate to the selected fragment
- navController.navigate(it.itemId)
+ binding.bottomNav.setOnItemSelectedListener {
+ navigateToBottomSelectedItem(it)
false
}
@@ -127,7 +127,7 @@ class MainActivity : BaseActivity() {
val log = PreferenceHelper.getErrorLog()
if (log != "") ErrorDialog().show(supportFragmentManager, null)
- setupBreakReminder()
+ BreakReminder.setupBreakReminder(applicationContext)
setupSubscriptionsBadge()
@@ -143,56 +143,38 @@ class MainActivity : BaseActivity() {
}
}
- if (navController.currentDestination?.id == startFragmentId) {
- moveTaskToBack(true)
- } else {
- navController.popBackStack()
+ when (navController.currentDestination?.id) {
+ startFragmentId -> {
+ moveTaskToBack(true)
+ }
+ R.id.searchResultFragment -> {
+ navController.popBackStack(R.id.searchFragment, true) ||
+ navController.popBackStack()
+ }
+ else -> {
+ navController.popBackStack()
+ }
}
}
})
+
+ loadIntentData()
}
/**
- * Show a break reminder when watched too long
+ * Rotate according to the preference
*/
- private fun setupBreakReminder() {
- if (!PreferenceHelper.getBoolean(
- PreferenceKeys.BREAK_REMINDER_TOGGLE,
- false
- )
- ) {
- return
+ fun requestOrientationChange() {
+ requestedOrientation = if (autoRotationEnabled) {
+ ActivityInfo.SCREEN_ORIENTATION_USER
+ } else {
+ ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
- val breakReminderPref = PreferenceHelper.getString(
- PreferenceKeys.BREAK_REMINDER,
- "0"
- )
- if (!breakReminderPref.all { Character.isDigit(it) } ||
- breakReminderPref == "" || breakReminderPref == "0"
- ) {
- return
- }
- Handler(Looper.getMainLooper()).postDelayed(
- {
- try {
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.share_with_time))
- .setMessage(
- getString(
- R.string.already_spent_time,
- breakReminderPref
- )
- )
- .setPositiveButton(R.string.okay, null)
- .show()
- } catch (e: Exception) {
- kotlin.runCatching {
- Toast.makeText(this, R.string.take_a_break, Toast.LENGTH_LONG).show()
- }
- }
- },
- breakReminderPref.toLong() * 60 * 1000
- )
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+ menu?.findItem(R.id.action_queue)?.isVisible = PlayingQueue.isNotEmpty()
+ return super.onPrepareOptionsMenu(menu)
}
/**
@@ -227,6 +209,8 @@ class MainActivity : BaseActivity() {
private fun removeSearchFocus() {
searchView.setQuery("", false)
searchView.clearFocus()
+ searchView.isIconified = true
+ searchItem.collapseActionView()
searchView.onActionViewCollapsed()
}
@@ -236,27 +220,31 @@ class MainActivity : BaseActivity() {
// stuff for the search in the topBar
val searchItem = menu.findItem(R.id.action_search)
+ this.searchItem = searchItem
searchView = searchItem.actionView as SearchView
val searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
- searchView.setOnSearchClickListener {
- if (navController.currentDestination?.id != R.id.searchResultFragment) {
- searchViewModel.setQuery(null)
- navController.navigate(R.id.searchFragment)
- }
- }
-
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
val bundle = Bundle()
bundle.putString("query", query)
navController.navigate(R.id.searchResultFragment, bundle)
searchViewModel.setQuery("")
+ searchView.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
+ // Prevent navigation when search view is collapsed
+ if (searchView.isIconified ||
+ binding.bottomNav.menu.children.any {
+ it.itemId == navController.currentDestination?.id
+ }
+ ) {
+ return true
+ }
+
// prevent malicious navigation when the search view is getting collapsed
if (navController.currentDestination?.id in listOf(
R.id.searchResultFragment,
@@ -279,6 +267,36 @@ class MainActivity : BaseActivity() {
return true
}
})
+
+ searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
+ if (navController.currentDestination?.id != R.id.searchResultFragment) {
+ searchViewModel.setQuery(null)
+ navController.navigate(R.id.searchFragment)
+ }
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+ if (binding.mainMotionLayout.progress == 0F) {
+ try {
+ minimizePlayer()
+ } catch (e: Exception) {
+ // current fragment isn't the player fragment
+ }
+ }
+ // Handover back press to `BackPressedDispatcher`
+ else if (binding.bottomNav.menu.children.none {
+ it.itemId == navController.currentDestination?.id
+ }
+ ) {
+ this@MainActivity.onBackPressedDispatcher.onBackPressed()
+ }
+
+ return true
+ }
+ })
return super.onCreateOptionsMenu(menu)
}
@@ -302,16 +320,14 @@ class MainActivity : BaseActivity() {
startActivity(communityIntent)
true
}
+ R.id.action_queue -> {
+ PlayingQueueSheet().show(supportFragmentManager, null)
+ true
+ }
else -> super.onOptionsItemSelected(item)
}
}
- override fun onStart() {
- super.onStart()
- // check whether an URI got submitted over the intent data and load it
- loadIntentData()
- }
-
private fun loadIntentData() {
intent?.getStringExtra(IntentData.channelId)?.let {
navController.navigate(
@@ -334,6 +350,20 @@ class MainActivity : BaseActivity() {
intent?.getStringExtra(IntentData.videoId)?.let {
loadVideo(it, intent?.getLongExtra(IntentData.timeStamp, 0L))
}
+ when (intent?.getStringExtra("fragmentToOpen")) {
+ "home" ->
+ navController.navigate(R.id.homeFragment)
+ "trends" ->
+ navController.navigate(R.id.trendsFragment)
+ "subscriptions" ->
+ navController.navigate(R.id.subscriptionsFragment)
+ "library" ->
+ navController.navigate(R.id.libraryFragment)
+ }
+ if (intent?.getBooleanExtra(IntentData.openQueueOnce, false) == true) {
+ PlayingQueueSheet()
+ .show(supportFragmentManager)
+ }
}
private fun loadVideo(videoId: String, timeStamp: Long?) {
@@ -359,7 +389,7 @@ class MainActivity : BaseActivity() {
transitionToStart()
}
}
- }, 100)
+ }, 300)
}
private fun minimizePlayer() {
@@ -459,6 +489,26 @@ class MainActivity : BaseActivity() {
}
}
+ private fun navigateToBottomSelectedItem(item: MenuItem) {
+ // clear backstack if it's the start fragment
+ if (startFragmentId == item.itemId) navController.backQueue.clear()
+
+ if (item.itemId == R.id.subscriptionsFragment) {
+ binding.bottomNav.removeBadge(R.id.subscriptionsFragment)
+ }
+
+ // navigate to the selected fragment, if the fragment already
+ // exists in backstack then pop up to that entry
+ if (!navController.popBackStack(item.itemId, false)) {
+ navController.navigate(item.itemId)
+ }
+
+ // Remove focus from search view when navigating to bottom view.
+ // Call only after navigate to destination, so it can be used in
+ // onMenuItemActionCollapse for backstack management
+ removeSearchFocus()
+ }
+
override fun onUserLeaveHint() {
super.onUserLeaveHint()
supportFragmentManager.fragments.forEach { fragment ->
diff --git a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt
index 2e717ba73..4f6d26638 100644
--- a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt
+++ b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt
@@ -68,28 +68,26 @@ class OfflinePlayerActivity : BaseActivity() {
}
binding.player.initialize(
- supportFragmentManager,
null,
binding.doubleTapOverlay.binding,
null
)
}
+ private fun File.toUri(): Uri? {
+ return if (this.exists()) Uri.fromFile(this) else null
+ }
+
private fun playVideo() {
- val videoDownloadDir = DownloadHelper.getVideoDir(this)
- val videoFile = File(
- videoDownloadDir,
+ val videoUri = File(
+ DownloadHelper.getDownloadDir(this, DownloadHelper.VIDEO_DIR),
fileName
- )
+ ).toUri()
- val audioDownloadDir = DownloadHelper.getAudioDir(this)
- val audioFile = File(
- audioDownloadDir,
+ val audioUri = File(
+ DownloadHelper.getDownloadDir(this, DownloadHelper.AUDIO_DIR),
fileName
- )
-
- val videoUri = if (videoFile.exists()) Uri.fromFile(videoFile) else null
- val audioUri = if (audioFile.exists()) Uri.fromFile(audioFile) else null
+ ).toUri()
setMediaSource(
videoUri,
diff --git a/app/src/main/java/com/github/libretube/ui/activities/RouterActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/RouterActivity.kt
index e66f02fd2..e231584e6 100644
--- a/app/src/main/java/com/github/libretube/ui/activities/RouterActivity.kt
+++ b/app/src/main/java/com/github/libretube/ui/activities/RouterActivity.kt
@@ -10,6 +10,7 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.extensions.TAG
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.util.NavigationHelper
+import kotlin.time.Duration
class RouterActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -75,14 +76,14 @@ class RouterActivity : BaseActivity() {
intent.putExtra(IntentData.videoId, videoId)
uri.getQueryParameter("t")
- ?.let { intent.putExtra(IntentData.timeStamp, it.toLong()) }
+ ?.let { intent.putExtra(IntentData.timeStamp, parseTimestamp(it)) }
}
else -> {
val videoId = uri.path!!.replace("/", "")
intent.putExtra(IntentData.videoId, videoId)
uri.getQueryParameter("t")
- ?.let { intent.putExtra(IntentData.timeStamp, it.toLong()) }
+ ?.let { intent.putExtra(IntentData.timeStamp, parseTimestamp(it)) }
}
}
return intent
@@ -99,4 +100,12 @@ class RouterActivity : BaseActivity() {
)
this.finishAndRemoveTask()
}
+
+ private fun parseTimestamp(t: String): Long? {
+ if (t.all { c -> c.isDigit() }) {
+ return t.toLong()
+ }
+
+ return Duration.parseOrNull(t)?.inWholeSeconds
+ }
}
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/BottomSheetAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/BottomSheetAdapter.kt
index e694e8f5a..f17ca2535 100644
--- a/app/src/main/java/com/github/libretube/ui/adapters/BottomSheetAdapter.kt
+++ b/app/src/main/java/com/github/libretube/ui/adapters/BottomSheetAdapter.kt
@@ -24,16 +24,17 @@ class BottomSheetAdapter(
override fun onBindViewHolder(holder: BottomSheetViewHolder, position: Int) {
val item = items[position]
holder.binding.apply {
+ val current = item.getCurrent()
title.text =
- if (item.currentValue != null) "${item.title} (${item.currentValue})" else item.title
+ if (current != null) "${item.title} ($current)" else item.title
if (item.drawable != null) {
drawable.setImageResource(item.drawable)
} else {
- drawable.visibility =
- View.GONE
+ drawable.visibility = View.GONE
}
root.setOnClickListener {
+ item.onClick.invoke()
listener.invoke(position)
}
}
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/ChannelAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/ChannelAdapter.kt
deleted file mode 100644
index d0467ad4b..000000000
--- a/app/src/main/java/com/github/libretube/ui/adapters/ChannelAdapter.kt
+++ /dev/null
@@ -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,
- private val childFragmentManager: FragmentManager,
- private val showChannelInfo: Boolean = false
-) :
- RecyclerView.Adapter() {
-
- override fun getItemCount(): Int {
- return videoFeed.size
- }
-
- fun updateItems(newItems: List) {
- 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!!)
- }
- }
-}
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt
index b0c65a972..8b7f64a68 100644
--- a/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt
+++ b/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt
@@ -1,9 +1,11 @@
package com.github.libretube.ui.adapters
import android.graphics.Color
+import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
+import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.databinding.ChapterColumnBinding
import com.github.libretube.ui.viewholders.ChaptersViewHolder
import com.github.libretube.util.ImageHelper
@@ -11,7 +13,7 @@ import com.github.libretube.util.ThemeHelper
import com.google.android.exoplayer2.ExoPlayer
class ChaptersAdapter(
- private val chapters: List,
+ private val chapters: List,
private val exoPlayer: ExoPlayer
) : RecyclerView.Adapter() {
private var selectedPosition = 0
@@ -27,15 +29,15 @@ class ChaptersAdapter(
holder.binding.apply {
ImageHelper.loadImage(chapter.image, chapterImage)
chapterTitle.text = chapter.title
+ timeStamp.text = chapter.start?.let { DateUtils.formatElapsedTime(it) }
- if (selectedPosition == position) {
- // get the color for highlighted controls
- val color =
- ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
- chapterLL.setBackgroundColor(color)
+ val color = if (selectedPosition == position) {
+ ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
} else {
- chapterLL.setBackgroundColor(Color.TRANSPARENT)
+ Color.TRANSPARENT
}
+ chapterLL.setBackgroundColor(color)
+
root.setOnClickListener {
updateSelectedPosition(position)
val chapterStart = chapter.start!! * 1000 // s -> ms
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt
index c54d089ae..330f6e543 100644
--- a/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt
+++ b/app/src/main/java/com/github/libretube/ui/adapters/CommentsAdapter.kt
@@ -5,6 +5,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Button
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -19,6 +20,7 @@ import com.github.libretube.ui.viewholders.CommentsViewHolder
import com.github.libretube.util.ClipboardHelper
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
+import com.github.libretube.util.TextUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -60,7 +62,7 @@ class CommentsAdapter(
root.scaleY = 0.9f
}
- commentInfos.text = comment.author.toString() + " • " + comment.commentedTime.toString()
+ commentInfos.text = comment.author.toString() + TextUtils.SEPARATOR + comment.commentedTime.toString()
commentText.text = comment.commentText.toString()
ImageHelper.loadImage(comment.thumbnail, commentorImage)
@@ -71,8 +73,7 @@ class CommentsAdapter(
if (comment.hearted == true) heartedImageView.visibility = View.VISIBLE
if (comment.repliesPage != null) repliesAvailable.visibility = View.VISIBLE
if ((comment.replyCount ?: -1L) > 0L) {
- repliesCount.text =
- comment.replyCount?.formatShort()
+ repliesCount.text = comment.replyCount?.formatShort()
}
commentorImage.setOnClickListener {
@@ -89,30 +90,7 @@ class CommentsAdapter(
repliesRecView.adapter = repliesAdapter
if (!isRepliesAdapter && comment.repliesPage != null) {
root.setOnClickListener {
- when {
- repliesAdapter.itemCount.equals(0) -> {
- fetchReplies(comment.repliesPage) {
- repliesAdapter.updateItems(it.comments)
- if (repliesPage.nextpage == null) {
- showMore.visibility = View.GONE
- return@fetchReplies
- }
- showMore.visibility = View.VISIBLE
- showMore.setOnClickListener {
- if (repliesPage.nextpage == null) {
- it.visibility = View.GONE
- return@setOnClickListener
- }
- fetchReplies(
- repliesPage.nextpage!!
- ) {
- repliesAdapter.updateItems(repliesPage.comments)
- }
- }
- }
- }
- else -> repliesAdapter.clear()
- }
+ showMoreReplies(comment.repliesPage, showMore, repliesAdapter)
}
}
@@ -124,6 +102,36 @@ class CommentsAdapter(
}
}
+ private fun showMoreReplies(nextPage: String, showMoreBtn: Button, repliesAdapter: CommentsAdapter) {
+ when {
+ repliesAdapter.itemCount.equals(0) -> {
+ fetchReplies(nextPage) {
+ repliesAdapter.updateItems(it.comments)
+ if (repliesPage.nextpage == null) {
+ showMoreBtn.visibility = View.GONE
+ return@fetchReplies
+ }
+ showMoreBtn.visibility = View.VISIBLE
+ showMoreBtn.setOnClickListener {
+ if (repliesPage.nextpage == null) {
+ it.visibility = View.GONE
+ return@setOnClickListener
+ }
+ fetchReplies(
+ repliesPage.nextpage!!
+ ) {
+ repliesAdapter.updateItems(repliesPage.comments)
+ }
+ }
+ }
+ }
+ else -> {
+ repliesAdapter.clear()
+ showMoreBtn.visibility = View.GONE
+ }
+ }
+ }
+
override fun getItemCount(): Int {
return comments.size
}
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt
index b1f35725b..3ecbcb932 100644
--- a/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt
+++ b/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt
@@ -13,6 +13,7 @@ import com.github.libretube.obj.DownloadedFile
import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.github.libretube.util.DownloadHelper
+import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
@@ -39,7 +40,7 @@ class DownloadsAdapter(
uploaderName.text = it.uploader
videoInfo.text = it.views.formatShort() + " " +
root.context.getString(R.string.views_placeholder) +
- " • " + it.uploadDate
+ TextUtils.SEPARATOR + it.uploadDate
}
thumbnailImage.setImageBitmap(file.thumbnail)
@@ -60,8 +61,8 @@ class DownloadsAdapter(
) { _, index ->
when (index) {
0 -> {
- val audioDir = DownloadHelper.getAudioDir(root.context)
- val videoDir = DownloadHelper.getVideoDir(root.context)
+ val audioDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.AUDIO_DIR)
+ val videoDir = DownloadHelper.getDownloadDir(root.context, DownloadHelper.VIDEO_DIR)
listOf(audioDir, videoDir).forEach {
val f = File(it, file.name)
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/IconsSheetAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/IconsSheetAdapter.kt
new file mode 100644
index 000000000..a1805b683
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/ui/adapters/IconsSheetAdapter.kt
@@ -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() {
+ 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
+ )
+ }
+}
diff --git a/app/src/main/java/com/github/libretube/ui/adapters/NavBarOptionsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/NavBarOptionsAdapter.kt
index 41fbad960..cdb366228 100644
--- a/app/src/main/java/com/github/libretube/ui/adapters/NavBarOptionsAdapter.kt
+++ b/app/src/main/java/com/github/libretube/ui/adapters/NavBarOptionsAdapter.kt
@@ -1,16 +1,16 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
+import android.view.MenuItem
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.databinding.NavOptionsItemBinding
-import com.github.libretube.obj.NavBarItem
import com.github.libretube.ui.viewholders.NavBarOptionsViewHolder
class NavBarOptionsAdapter(
- val items: MutableList
+ val items: MutableList