mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-13 22:00:30 +05:30
Merge pull request #5871 from Bnyro/master
feat: chapters support for downloaded videos
This commit is contained in:
commit
aef1d0c965
577
app/schemas/com.github.libretube.db.AppDatabase/18.json
Normal file
577
app/schemas/com.github.libretube.db.AppDatabase/18.json
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 18,
|
||||||
|
"identityHash": "e855c945119df154adaf1ecfe9e3b646",
|
||||||
|
"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, `isShort` INTEGER NOT NULL, 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShort",
|
||||||
|
"columnName": "isShort",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"query"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"channelId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, `videos` INTEGER NOT NULL, 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videos",
|
||||||
|
"columnName": "videos",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, `description` TEXT)",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "download",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `uploader` TEXT NOT NULL, `duration` INTEGER DEFAULT NULL, `uploadDate` TEXT, `thumbnailPath` TEXT, PRIMARY KEY(`videoId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "uploadDate",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailPath",
|
||||||
|
"columnName": "thumbnailPath",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloadItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `videoId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `path` TEXT NOT NULL, `url` TEXT, `format` TEXT, `quality` TEXT, `language` TEXT, `downloadSize` INTEGER NOT NULL, FOREIGN KEY(`videoId`) REFERENCES `download`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileName",
|
||||||
|
"columnName": "fileName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "format",
|
||||||
|
"columnName": "format",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "quality",
|
||||||
|
"columnName": "quality",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadSize",
|
||||||
|
"columnName": "downloadSize",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_downloadItem_path",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloadItem_path` ON `${TABLE_NAME}` (`path`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "download",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"videoId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloadChapters",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `videoId` TEXT NOT NULL, `name` TEXT NOT NULL, `start` INTEGER NOT NULL, `thumbnailUrl` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "start",
|
||||||
|
"columnName": "start",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "subscriptionGroups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channels",
|
||||||
|
"columnName": "channels",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, 'e855c945119df154adaf1ecfe9e3b646')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import com.github.libretube.db.dao.WatchHistoryDao
|
|||||||
import com.github.libretube.db.dao.WatchPositionDao
|
import com.github.libretube.db.dao.WatchPositionDao
|
||||||
import com.github.libretube.db.obj.CustomInstance
|
import com.github.libretube.db.obj.CustomInstance
|
||||||
import com.github.libretube.db.obj.Download
|
import com.github.libretube.db.obj.Download
|
||||||
|
import com.github.libretube.db.obj.DownloadChapter
|
||||||
import com.github.libretube.db.obj.DownloadItem
|
import com.github.libretube.db.obj.DownloadItem
|
||||||
import com.github.libretube.db.obj.LocalPlaylist
|
import com.github.libretube.db.obj.LocalPlaylist
|
||||||
import com.github.libretube.db.obj.LocalPlaylistItem
|
import com.github.libretube.db.obj.LocalPlaylistItem
|
||||||
@ -37,9 +38,10 @@ import com.github.libretube.db.obj.WatchPosition
|
|||||||
LocalPlaylistItem::class,
|
LocalPlaylistItem::class,
|
||||||
Download::class,
|
Download::class,
|
||||||
DownloadItem::class,
|
DownloadItem::class,
|
||||||
|
DownloadChapter::class,
|
||||||
SubscriptionGroup::class
|
SubscriptionGroup::class
|
||||||
],
|
],
|
||||||
version = 17,
|
version = 18,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 7, to = 8),
|
AutoMigration(from = 7, to = 8),
|
||||||
AutoMigration(from = 8, to = 9),
|
AutoMigration(from = 8, to = 9),
|
||||||
|
@ -44,6 +44,18 @@ object DatabaseHolder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_17_18 = object : Migration(17, 18) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("CREATE TABLE 'downloadChapters' (" +
|
||||||
|
"id INTEGER PRIMARY KEY NOT NULL, " +
|
||||||
|
"videoId TEXT NOT NULL, " +
|
||||||
|
"name TEXT NOT NULL, " +
|
||||||
|
"start INTEGER NOT NULL, " +
|
||||||
|
"thumbnailUrl TEXT NOT NULL" +
|
||||||
|
")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val Database by lazy {
|
val Database by lazy {
|
||||||
Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME)
|
Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME)
|
||||||
.addMigrations(
|
.addMigrations(
|
||||||
@ -51,7 +63,8 @@ object DatabaseHolder {
|
|||||||
MIGRATION_12_13,
|
MIGRATION_12_13,
|
||||||
MIGRATION_13_14,
|
MIGRATION_13_14,
|
||||||
MIGRATION_14_15,
|
MIGRATION_14_15,
|
||||||
MIGRATION_15_16
|
MIGRATION_15_16,
|
||||||
|
MIGRATION_17_18
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
|
@ -8,6 +8,7 @@ import androidx.room.Query
|
|||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import com.github.libretube.db.obj.Download
|
import com.github.libretube.db.obj.Download
|
||||||
|
import com.github.libretube.db.obj.DownloadChapter
|
||||||
import com.github.libretube.db.obj.DownloadItem
|
import com.github.libretube.db.obj.DownloadItem
|
||||||
import com.github.libretube.db.obj.DownloadWithItems
|
import com.github.libretube.db.obj.DownloadWithItems
|
||||||
|
|
||||||
@ -30,6 +31,9 @@ interface DownloadDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertDownload(download: Download)
|
suspend fun insertDownload(download: Download)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertDownloadChapter(downloadChapter: DownloadChapter)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertDownloadItem(downloadItem: DownloadItem): Long
|
suspend fun insertDownloadItem(downloadItem: DownloadItem): Long
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.github.libretube.db.obj
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
|
|
||||||
|
@Entity(tableName = "downloadChapters")
|
||||||
|
data class DownloadChapter(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
|
val videoId: String,
|
||||||
|
val name: String,
|
||||||
|
val start: Long,
|
||||||
|
val thumbnailUrl: String
|
||||||
|
) {
|
||||||
|
fun toChapterSegment(): ChapterSegment {
|
||||||
|
return ChapterSegment(name, thumbnailUrl, start)
|
||||||
|
}
|
||||||
|
}
|
@ -9,5 +9,10 @@ data class DownloadWithItems(
|
|||||||
parentColumn = "videoId",
|
parentColumn = "videoId",
|
||||||
entityColumn = "videoId"
|
entityColumn = "videoId"
|
||||||
)
|
)
|
||||||
val downloadItems: List<DownloadItem>
|
val downloadItems: List<DownloadItem>,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "videoId",
|
||||||
|
entityColumn = "videoId"
|
||||||
|
)
|
||||||
|
val downloadChapters: List<DownloadChapter> = emptyList()
|
||||||
)
|
)
|
||||||
|
@ -23,6 +23,7 @@ import com.github.libretube.api.RetrofitInstance
|
|||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.db.DatabaseHolder.Database
|
import com.github.libretube.db.DatabaseHolder.Database
|
||||||
import com.github.libretube.db.obj.Download
|
import com.github.libretube.db.obj.Download
|
||||||
|
import com.github.libretube.db.obj.DownloadChapter
|
||||||
import com.github.libretube.db.obj.DownloadItem
|
import com.github.libretube.db.obj.DownloadItem
|
||||||
import com.github.libretube.enums.FileType
|
import com.github.libretube.enums.FileType
|
||||||
import com.github.libretube.enums.NotificationId
|
import com.github.libretube.enums.NotificationId
|
||||||
@ -124,6 +125,15 @@ class DownloadService : LifecycleService() {
|
|||||||
thumbnailTargetPath
|
thumbnailTargetPath
|
||||||
)
|
)
|
||||||
Database.downloadDao().insertDownload(download)
|
Database.downloadDao().insertDownload(download)
|
||||||
|
for (chapter in streams.chapters) {
|
||||||
|
val downloadChapter = DownloadChapter(
|
||||||
|
videoId = videoId,
|
||||||
|
name = chapter.title,
|
||||||
|
start = chapter.start,
|
||||||
|
thumbnailUrl = chapter.image
|
||||||
|
)
|
||||||
|
Database.downloadDao().insertDownloadChapter(downloadChapter)
|
||||||
|
}
|
||||||
ImageHelper.downloadImage(
|
ImageHelper.downloadImage(
|
||||||
this@DownloadService,
|
this@DownloadService,
|
||||||
streams.thumbnailUrl,
|
streams.thumbnailUrl,
|
||||||
|
@ -28,6 +28,7 @@ import com.github.libretube.constants.IntentData
|
|||||||
import com.github.libretube.databinding.ActivityOfflinePlayerBinding
|
import com.github.libretube.databinding.ActivityOfflinePlayerBinding
|
||||||
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
||||||
import com.github.libretube.db.DatabaseHolder.Database
|
import com.github.libretube.db.DatabaseHolder.Database
|
||||||
|
import com.github.libretube.db.obj.DownloadChapter
|
||||||
import com.github.libretube.enums.FileType
|
import com.github.libretube.enums.FileType
|
||||||
import com.github.libretube.extensions.toAndroidUri
|
import com.github.libretube.extensions.toAndroidUri
|
||||||
import com.github.libretube.extensions.updateParameters
|
import com.github.libretube.extensions.updateParameters
|
||||||
@ -123,6 +124,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
player = PlayerHelper.createPlayer(this, trackSelector, false)
|
player = PlayerHelper.createPlayer(this, trackSelector, false)
|
||||||
player.setWakeMode(C.WAKE_MODE_LOCAL)
|
player.setWakeMode(C.WAKE_MODE_LOCAL)
|
||||||
player.addListener(playerListener)
|
player.addListener(playerListener)
|
||||||
|
playerViewModel.player = player
|
||||||
|
|
||||||
playerView = binding.player
|
playerView = binding.player
|
||||||
playerView.setShowSubtitleButton(true)
|
playerView.setShowSubtitleButton(true)
|
||||||
@ -146,6 +148,10 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
val downloadInfo = withContext(Dispatchers.IO) {
|
val downloadInfo = withContext(Dispatchers.IO) {
|
||||||
Database.downloadDao().findById(videoId)
|
Database.downloadDao().findById(videoId)
|
||||||
}
|
}
|
||||||
|
val chapters = downloadInfo.downloadChapters.map(DownloadChapter::toChapterSegment)
|
||||||
|
playerViewModel.chaptersLiveData.value = chapters
|
||||||
|
binding.player.setChapters(chapters)
|
||||||
|
|
||||||
val downloadFiles = downloadInfo.downloadItems.filter { it.path.exists() }
|
val downloadFiles = downloadInfo.downloadItems.filter { it.path.exists() }
|
||||||
playerBinding.exoTitle.text = downloadInfo.download.title
|
playerBinding.exoTitle.text = downloadInfo.download.title
|
||||||
playerBinding.exoTitle.isVisible = true
|
playerBinding.exoTitle.isVisible = true
|
||||||
@ -247,6 +253,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
saveWatchPosition()
|
saveWatchPosition()
|
||||||
|
|
||||||
|
playerViewModel.player = null
|
||||||
player.release()
|
player.release()
|
||||||
watchPositionTimer.destroy()
|
watchPositionTimer.destroy()
|
||||||
|
|
||||||
|
@ -102,7 +102,6 @@ import com.github.libretube.ui.listeners.SeekbarPreviewListener
|
|||||||
import com.github.libretube.ui.models.CommentsViewModel
|
import com.github.libretube.ui.models.CommentsViewModel
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||||
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
|
||||||
import com.github.libretube.ui.sheets.CommentsSheet
|
import com.github.libretube.ui.sheets.CommentsSheet
|
||||||
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
||||||
import com.github.libretube.ui.sheets.StatsSheet
|
import com.github.libretube.ui.sheets.StatsSheet
|
||||||
@ -166,8 +165,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
private var seekBarPreviewListener: SeekbarPreviewListener? = null
|
private var seekBarPreviewListener: SeekbarPreviewListener? = null
|
||||||
private var scrubbingTimeBar = false
|
|
||||||
private var chaptersBottomSheet: ChaptersBottomSheet? = null
|
|
||||||
|
|
||||||
// True when the video was closed through the close button on PiP mode
|
// True when the video was closed through the close button on PiP mode
|
||||||
private var closedVideo = false
|
private var closedVideo = false
|
||||||
@ -441,7 +438,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
disableController()
|
disableController()
|
||||||
commentsViewModel.setCommentSheetExpand(false)
|
commentsViewModel.setCommentSheetExpand(false)
|
||||||
chaptersBottomSheet?.dismiss()
|
|
||||||
transitionEndId = endId
|
transitionEndId = endId
|
||||||
transitionStartId = startId
|
transitionStartId = startId
|
||||||
}
|
}
|
||||||
@ -971,21 +967,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
// show the player notification
|
// show the player notification
|
||||||
initializePlayerNotification()
|
initializePlayerNotification()
|
||||||
|
|
||||||
// enable the chapters dialog in the player
|
binding.player.setCurrentChapterName()
|
||||||
playerBinding.chapterName.setOnClickListener {
|
|
||||||
updateMaxSheetHeight()
|
|
||||||
val sheet =
|
|
||||||
chaptersBottomSheet ?: ChaptersBottomSheet().also {
|
|
||||||
chaptersBottomSheet = it
|
|
||||||
}
|
|
||||||
if (sheet.isVisible) {
|
|
||||||
sheet.dismiss()
|
|
||||||
} else {
|
|
||||||
sheet.show(childFragmentManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentChapterName()
|
|
||||||
|
|
||||||
fetchSponsorBlockSegments()
|
fetchSponsorBlockSegments()
|
||||||
|
|
||||||
@ -1048,9 +1030,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
|
|
||||||
// close comment bottom sheet if opened for next video
|
// close comment bottom sheet if opened for next video
|
||||||
runCatching { commentsViewModel.commentsSheetDismiss?.invoke() }
|
runCatching { commentsViewModel.commentsSheetDismiss?.invoke() }
|
||||||
// kill the chapters bottom sheet if opened
|
|
||||||
runCatching { chaptersBottomSheet?.dismiss() }
|
|
||||||
chaptersBottomSheet = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
@ -1202,36 +1181,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
)
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setCurrentChapterName()
|
binding.player.setCurrentChapterName()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the name of the video chapter in the exoPlayerView
|
|
||||||
private fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
|
|
||||||
// return if fragment view got killed already to avoid crashes
|
|
||||||
if (_binding == null) return
|
|
||||||
|
|
||||||
// only show the chapters layout if there are some chapters available
|
|
||||||
playerBinding.chapterName.isInvisible = viewModel.chapters.isEmpty()
|
|
||||||
|
|
||||||
// the following logic to set the chapter title can be skipped if no chapters are available
|
|
||||||
if (viewModel.chapters.isEmpty()) return
|
|
||||||
|
|
||||||
// call the function again in 100ms
|
|
||||||
if (enqueueNew) binding.player.postDelayed(this::setCurrentChapterName, 100)
|
|
||||||
|
|
||||||
// if the user is scrubbing the time bar, don't update
|
|
||||||
if (scrubbingTimeBar && !forceUpdate) return
|
|
||||||
|
|
||||||
val chapterName =
|
|
||||||
PlayerHelper.getCurrentChapterIndex(exoPlayer.currentPosition, viewModel.chapters)
|
|
||||||
?.let {
|
|
||||||
viewModel.chapters[it].title.trim()
|
|
||||||
} ?: getString(R.string.no_chapter)
|
|
||||||
|
|
||||||
// change the chapter name textView text to the chapterName
|
|
||||||
if (chapterName != playerBinding.chapterName.text) {
|
|
||||||
playerBinding.chapterName.text = chapterName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1608,15 +1558,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
return SeekbarPreviewListener(
|
return SeekbarPreviewListener(
|
||||||
OnlineTimeFrameReceiver(requireContext(), streams.previewFrames),
|
OnlineTimeFrameReceiver(requireContext(), streams.previewFrames),
|
||||||
playerBinding,
|
playerBinding,
|
||||||
streams.duration * 1000,
|
streams.duration * 1000
|
||||||
onScrub = {
|
|
||||||
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
|
|
||||||
scrubbingTimeBar = true
|
|
||||||
},
|
|
||||||
onScrubEnd = {
|
|
||||||
scrubbingTimeBar = false
|
|
||||||
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ import kotlinx.coroutines.withContext
|
|||||||
class SeekbarPreviewListener(
|
class SeekbarPreviewListener(
|
||||||
private val timeFrameReceiver: TimeFrameReceiver,
|
private val timeFrameReceiver: TimeFrameReceiver,
|
||||||
private val playerBinding: ExoStyledPlayerControlViewBinding,
|
private val playerBinding: ExoStyledPlayerControlViewBinding,
|
||||||
private val duration: Long,
|
private val duration: Long
|
||||||
private val onScrub: (position: Long) -> Unit = {},
|
|
||||||
private val onScrubEnd: (position: Long) -> Unit = {}
|
|
||||||
) : TimeBar.OnScrubListener {
|
) : TimeBar.OnScrubListener {
|
||||||
private var scrubInProgress = false
|
private var scrubInProgress = false
|
||||||
private var lastPreviewPosition = Long.MAX_VALUE
|
private var lastPreviewPosition = Long.MAX_VALUE
|
||||||
@ -52,10 +50,6 @@ class SeekbarPreviewListener(
|
|||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
processPreview(position)
|
processPreview(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching {
|
|
||||||
onScrub.invoke(position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,8 +68,6 @@ class SeekbarPreviewListener(
|
|||||||
playerBinding.seekbarPreview.alpha = 1f
|
playerBinding.seekbarPreview.alpha = 1f
|
||||||
}
|
}
|
||||||
.start()
|
.start()
|
||||||
|
|
||||||
onScrubEnd.invoke(position)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package com.github.libretube.ui.models
|
package com.github.libretube.ui.models
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -25,6 +24,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
class PlayerViewModel : ViewModel() {
|
class PlayerViewModel : ViewModel() {
|
||||||
var player: ExoPlayer? = null
|
var player: ExoPlayer? = null
|
||||||
var trackSelector: DefaultTrackSelector? = null
|
var trackSelector: DefaultTrackSelector? = null
|
||||||
|
@ -9,6 +9,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.databinding.BottomSheetBinding
|
import com.github.libretube.databinding.BottomSheetBinding
|
||||||
@ -16,6 +17,7 @@ import com.github.libretube.helpers.PlayerHelper
|
|||||||
import com.github.libretube.ui.adapters.ChaptersAdapter
|
import com.github.libretube.ui.adapters.ChaptersAdapter
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
class ChaptersBottomSheet : UndimmedBottomSheet() {
|
class ChaptersBottomSheet : UndimmedBottomSheet() {
|
||||||
private var _binding: BottomSheetBinding? = null
|
private var _binding: BottomSheetBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
@ -22,6 +22,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginStart
|
import androidx.core.view.marginStart
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
@ -35,6 +36,7 @@ import androidx.media3.ui.PlayerView
|
|||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
import androidx.media3.ui.TimeBar
|
import androidx.media3.ui.TimeBar
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.databinding.DoubleTapOverlayBinding
|
import com.github.libretube.databinding.DoubleTapOverlayBinding
|
||||||
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
||||||
@ -58,13 +60,14 @@ import com.github.libretube.ui.interfaces.PlayerGestureOptions
|
|||||||
import com.github.libretube.ui.interfaces.PlayerOptions
|
import com.github.libretube.ui.interfaces.PlayerOptions
|
||||||
import com.github.libretube.ui.listeners.PlayerGestureController
|
import com.github.libretube.ui.listeners.PlayerGestureController
|
||||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||||
|
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
||||||
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
||||||
import com.github.libretube.ui.sheets.SleepTimerSheet
|
import com.github.libretube.ui.sheets.SleepTimerSheet
|
||||||
import com.github.libretube.util.PlayingQueue
|
import com.github.libretube.util.PlayingQueue
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
open class CustomExoPlayerView(
|
abstract class CustomExoPlayerView(
|
||||||
context: Context,
|
context: Context,
|
||||||
attributeSet: AttributeSet? = null
|
attributeSet: AttributeSet? = null
|
||||||
) : PlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
|
) : PlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
|
||||||
@ -79,6 +82,8 @@ open class CustomExoPlayerView(
|
|||||||
private lateinit var brightnessHelper: BrightnessHelper
|
private lateinit var brightnessHelper: BrightnessHelper
|
||||||
private lateinit var audioHelper: AudioHelper
|
private lateinit var audioHelper: AudioHelper
|
||||||
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
|
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
|
||||||
|
private var chaptersBottomSheet: ChaptersBottomSheet? = null
|
||||||
|
private var scrubbingTimeBar = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Objects from the parent fragment
|
* Objects from the parent fragment
|
||||||
@ -191,10 +196,16 @@ open class CustomExoPlayerView(
|
|||||||
|
|
||||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||||
cancelHideControllerTask()
|
cancelHideControllerTask()
|
||||||
|
|
||||||
|
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
|
||||||
|
scrubbingTimeBar = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||||
enqueueHideControllerTask()
|
enqueueHideControllerTask()
|
||||||
|
|
||||||
|
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
|
||||||
|
scrubbingTimeBar = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -217,6 +228,46 @@ open class CustomExoPlayerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentPosition()
|
updateCurrentPosition()
|
||||||
|
|
||||||
|
// enable the chapters dialog in the player
|
||||||
|
binding.chapterName.setOnClickListener {
|
||||||
|
val sheet = chaptersBottomSheet ?: ChaptersBottomSheet().also {
|
||||||
|
chaptersBottomSheet = it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sheet.isVisible) {
|
||||||
|
sheet.dismiss()
|
||||||
|
} else {
|
||||||
|
sheet.show(activity.supportFragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the name of the video chapter in the exoPlayerView
|
||||||
|
fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
|
||||||
|
val player = player ?: return
|
||||||
|
val chapters = getChapters()
|
||||||
|
|
||||||
|
binding.chapterName.isInvisible = chapters.isEmpty()
|
||||||
|
|
||||||
|
// the following logic to set the chapter title can be skipped if no chapters are available
|
||||||
|
if (chapters.isEmpty()) return
|
||||||
|
|
||||||
|
// call the function again in 100ms
|
||||||
|
if (enqueueNew) postDelayed(this::setCurrentChapterName, 100)
|
||||||
|
|
||||||
|
// if the user is scrubbing the time bar, don't update
|
||||||
|
if (scrubbingTimeBar && !forceUpdate) return
|
||||||
|
|
||||||
|
val newChapterName =
|
||||||
|
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chapters)
|
||||||
|
?.let { chapters[it].title.trim() }
|
||||||
|
?: context.getString(R.string.no_chapter)
|
||||||
|
|
||||||
|
// change the chapter name textView text to the chapterName
|
||||||
|
if (newChapterName != binding.chapterName.text) {
|
||||||
|
binding.chapterName.text = newChapterName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSystemBars(showBars: Boolean) {
|
fun toggleSystemBars(showBars: Boolean) {
|
||||||
@ -771,6 +822,8 @@ open class CustomExoPlayerView(
|
|||||||
|
|
||||||
open fun minimizeOrExitPlayer() = Unit
|
open fun minimizeOrExitPlayer() = Unit
|
||||||
|
|
||||||
|
abstract fun getChapters(): List<ChapterSegment>
|
||||||
|
|
||||||
open fun getWindow(): Window = activity.window
|
open fun getWindow(): Window = activity.window
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -3,11 +3,14 @@ package com.github.libretube.ui.views
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
|
|
||||||
class OfflinePlayerView(
|
class OfflinePlayerView(
|
||||||
context: Context,
|
context: Context,
|
||||||
attributeSet: AttributeSet? = null
|
attributeSet: AttributeSet? = null
|
||||||
) : CustomExoPlayerView(context, attributeSet) {
|
) : CustomExoPlayerView(context, attributeSet) {
|
||||||
|
private var chapters: List<ChapterSegment> = emptyList()
|
||||||
|
|
||||||
override fun hideController() {
|
override fun hideController() {
|
||||||
super.hideController()
|
super.hideController()
|
||||||
// hide the status bars when continuing to watch video
|
// hide the status bars when continuing to watch video
|
||||||
@ -23,4 +26,11 @@ class OfflinePlayerView(
|
|||||||
override fun minimizeOrExitPlayer() {
|
override fun minimizeOrExitPlayer() {
|
||||||
(context as AppCompatActivity).onBackPressedDispatcher.onBackPressed()
|
(context as AppCompatActivity).onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setChapters(chapters: List<ChapterSegment>) {
|
||||||
|
this.chapters = chapters
|
||||||
|
setCurrentChapterName()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapters(): List<ChapterSegment> = chapters
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,10 @@ import androidx.core.os.bundleOf
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.trackselection.TrackSelector
|
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
@ -22,6 +24,7 @@ import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
|||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.util.PlayingQueue
|
import com.github.libretube.util.PlayingQueue
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
class OnlinePlayerView(
|
class OnlinePlayerView(
|
||||||
context: Context,
|
context: Context,
|
||||||
attributeSet: AttributeSet? = null
|
attributeSet: AttributeSet? = null
|
||||||
@ -200,4 +203,8 @@ class OnlinePlayerView(
|
|||||||
override fun minimizeOrExitPlayer() {
|
override fun minimizeOrExitPlayer() {
|
||||||
playerOptions?.exitFullscreen()
|
playerOptions?.exitFullscreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getChapters(): List<ChapterSegment> {
|
||||||
|
return playerViewModel?.chapters.orEmpty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user