mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-02 15:46:45 +00:00
Merge branch 'master' into 'subscription-submission-modal'
# Conflicts: # app/src/main/res/values/strings.xml
This commit is contained in:
commit
a69692be18
158 changed files with 1088 additions and 292 deletions
|
@ -49,9 +49,23 @@ We encourage developers to write their own plugins. Please refer to the "Getting
|
||||||
|
|
||||||
## Contributing to Core
|
## Contributing to Core
|
||||||
|
|
||||||
**We are currently not accepting contributions to the core.**
|
|
||||||
|
|
||||||
The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity.
|
### License
|
||||||
|
|
||||||
|
The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/).
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
|
||||||
|
1. Fork the core repository.
|
||||||
|
2. Clone your fork.
|
||||||
|
3. Make your changes.
|
||||||
|
4. Commit and push your changes.
|
||||||
|
5. Open a pull request.
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
- Ensure your code adheres to the existing style.
|
||||||
|
- Include documentation and unit tests (where applicable).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
35
README.md
35
README.md
|
@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Video</td>
|
<td>Video</td>
|
||||||
|
@ -24,12 +24,10 @@ The FUTO media app is a player that exposes multiple video websites as sources i
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Sources (all enabled)</td>
|
<td>Sources</td>
|
||||||
<td>Sources (one disabled)</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -38,7 +36,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Install a new source</td>
|
<td>Install a new source</td>
|
||||||
|
@ -54,8 +52,8 @@ When a user enters a search term into the search bar, the query is posted to th
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Search (list)</td>
|
<td>Search (list)</td>
|
||||||
|
@ -71,7 +69,7 @@ Creators are able to configure their profile using NeoPass.
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Channel</td>
|
<td>Channel</td>
|
||||||
|
@ -112,7 +110,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Settings</td>
|
<td>Settings</td>
|
||||||
|
@ -125,8 +123,8 @@ Playlists allow you to make a collection of videos that you can create and custo
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Playlists</td>
|
<td>Playlists</td>
|
||||||
|
@ -142,7 +140,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Downloads</td>
|
<td>Downloads</td>
|
||||||
|
@ -157,7 +155,7 @@ For more information about casting please click [here](./docs/casting.md).
|
||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Casting</td>
|
<td>Casting</td>
|
||||||
|
@ -182,6 +180,12 @@ In the future we hope to offer users the choice of their desired recommendation
|
||||||
|
|
||||||
1. Download a copy of the repository.
|
1. Download a copy of the repository.
|
||||||
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
|
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
|
||||||
|
3. Open the terminal in Android Studio by clicking on the terminal icon on bottom left and run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
|
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
|
||||||
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
|
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
|
||||||
|
|
||||||
|
@ -199,7 +203,6 @@ Create a tag on the master branch, incrementing the last version number by 1 (fo
|
||||||
|
|
||||||
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
|
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
|
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
|
||||||
|
|
|
@ -226,10 +226,6 @@
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncHomeActivity"
|
android:name=".activities.SyncHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
|
|
|
@ -150,7 +150,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
val intent = MainActivity.getImportOptionsIntent(act);
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
|
||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,6 +504,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
||||||
var autoplay: Boolean = false;
|
var autoplay: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
||||||
|
var deleteFromWatchLaterAuto: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
|
@ -862,10 +864,14 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
StatePayment.instance.clearLicenses();
|
SettingsActivity.getActivity()?.let { context ->
|
||||||
SettingsActivity.getActivity()?.let {
|
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
StatePayment.instance.clearLicenses();
|
||||||
it.reloadSettings();
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
|
it.reloadSettings();
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -878,7 +884,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
||||||
var bypassRotationPrevention: Boolean = false;
|
var bypassRotationPrevention: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -919,7 +928,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var enabled: Boolean = true;
|
var enabled: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
||||||
var broadcast: Boolean = true;
|
var broadcast: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
|
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
|
||||||
var connectDiscovered: Boolean = true;
|
var connectDiscovered: Boolean = true;
|
||||||
|
|
|
@ -350,6 +350,13 @@ class UIDialogs {
|
||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
|
||||||
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
|
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
||||||
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||||
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
val dialog = AutoUpdateDialog(context);
|
val dialog = AutoUpdateDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
@ -879,6 +880,12 @@ class UISlideOverlays {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||||
|
|
||||||
|
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||||
|
if (it is JSClient)
|
||||||
|
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
||||||
|
else false;
|
||||||
|
} ?: false;
|
||||||
|
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
|
@ -899,17 +906,18 @@ class UISlideOverlays {
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
SlideUpMenuItem(
|
if(!isLimited)
|
||||||
container.context,
|
SlideUpMenuItem(
|
||||||
R.drawable.ic_download,
|
container.context,
|
||||||
container.context.getString(R.string.download),
|
R.drawable.ic_download,
|
||||||
container.context.getString(R.string.download_the_video),
|
container.context.getString(R.string.download),
|
||||||
tag = "download",
|
container.context.getString(R.string.download_the_video),
|
||||||
call = {
|
tag = "download",
|
||||||
showDownloadVideoOverlay(video, container, true);
|
call = {
|
||||||
},
|
showDownloadVideoOverlay(video, container, true);
|
||||||
invokeParent = false
|
},
|
||||||
),
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_share,
|
R.drawable.ic_share,
|
||||||
|
@ -936,7 +944,7 @@ class UISlideOverlays {
|
||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
}))
|
}))
|
||||||
+ actions)
|
+ actions).filterNotNull()
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
|
@ -1033,15 +1041,7 @@ class UISlideOverlays {
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||||
SlideUpMenuItem(
|
)
|
||||||
container.context,
|
|
||||||
R.drawable.ic_download,
|
|
||||||
container.context.getString(R.string.download),
|
|
||||||
container.context.getString(R.string.download_the_video),
|
|
||||||
tag = container.context.getString(R.string.download),
|
|
||||||
call = { showDownloadVideoOverlay(video, container, true); },
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
|
|
@ -1310,7 +1310,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "TAB";
|
sourcesIntent.action = "TAB";
|
||||||
sourcesIntent.putExtra("TAB", tab);
|
sourcesIntent.putExtra("TAB", tab);
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1318,7 +1318,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "VIDEO";
|
sourcesIntent.action = "VIDEO";
|
||||||
sourcesIntent.putExtra("VIDEO", videoUrl);
|
sourcesIntent.putExtra("VIDEO", videoUrl);
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1326,14 +1326,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "ACTION";
|
sourcesIntent.action = "ACTION";
|
||||||
sourcesIntent.putExtra("ACTION", action);
|
sourcesIntent.putExtra("ACTION", action);
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImportOptionsIntent(context: Context): Intent {
|
fun getImportOptionsIntent(context: Context): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "IMPORT_OPTIONS";
|
sourcesIntent.action = "IMPORT_OPTIONS";
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,8 @@ class SourcePluginConfig(
|
||||||
var primaryClaimFieldType: Int? = null,
|
var primaryClaimFieldType: Int? = null,
|
||||||
var developerSubmitUrl: String? = null,
|
var developerSubmitUrl: String? = null,
|
||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
var maxDownloadParallelism: Int = 0
|
var maxDownloadParallelism: Int = 0,
|
||||||
|
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
|
|
|
@ -71,6 +71,8 @@ abstract class JSPager<T> : IPager<T> {
|
||||||
|
|
||||||
warnIfMainThread("JSPager.getResults");
|
warnIfMainThread("JSPager.getResults");
|
||||||
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
||||||
|
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
|
||||||
|
throw IllegalStateException("Runtime closed");
|
||||||
val newResults = items.toArray()
|
val newResults = items.toArray()
|
||||||
.map { convertResult(it as V8ValueObject) }
|
.map { convertResult(it as V8ValueObject) }
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
@ -22,6 +22,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
||||||
private lateinit var _buttonCancel: ImageButton;
|
private lateinit var _buttonCancel: ImageButton;
|
||||||
|
|
||||||
private lateinit var _editPassword: EditText;
|
private lateinit var _editPassword: EditText;
|
||||||
|
private lateinit var _editPassword2: EditText;
|
||||||
|
|
||||||
private lateinit var _inputMethodManager: InputMethodManager;
|
private lateinit var _inputMethodManager: InputMethodManager;
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStart = findViewById(R.id.button_start);
|
_buttonStart = findViewById(R.id.button_start);
|
||||||
_editPassword = findViewById(R.id.edit_password);
|
_editPassword = findViewById(R.id.edit_password);
|
||||||
|
_editPassword2 = findViewById(R.id.edit_password2);
|
||||||
|
|
||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
|
@ -52,6 +54,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonStart.setOnClickListener {
|
_buttonStart.setOnClickListener {
|
||||||
|
val p1 = _editPassword.text.toString();
|
||||||
|
val p2 = _editPassword2.text.toString();
|
||||||
|
if(!(p1?.equals(p2) ?: false)) {
|
||||||
|
UIDialogs.toast(context, "Password fields do not match, confirm that you typed it correctly.");
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
val pbytes = _editPassword.text.toString().toByteArray();
|
val pbytes = _editPassword.text.toString().toByteArray();
|
||||||
if(pbytes.size < 4 || pbytes.size > 32) {
|
if(pbytes.size < 4 || pbytes.size > 32) {
|
||||||
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
|
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.widget.TextView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.futopay.PaymentConfigurations
|
import com.futo.futopay.PaymentConfigurations
|
||||||
import com.futo.futopay.PaymentManager
|
import com.futo.futopay.PaymentManager
|
||||||
|
import com.futo.futopay.formatMoney
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
@ -94,9 +95,8 @@ class BuyFragment : MainFragment() {
|
||||||
|
|
||||||
if(currency != null && prices.containsKey(currency.id)) {
|
if(currency != null && prices.containsKey(currency.id)) {
|
||||||
val price = prices[currency.id]!!;
|
val price = prices[currency.id]!!;
|
||||||
val priceDecimal = (price.toDouble() / 100);
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_buttonBuyText.text = currency.symbol + String.format("%.2f", priceDecimal) + context.getString(R.string.plus_tax);
|
_buttonBuyText.text = formatMoney(country.id, currency.id, price) + context.getString(R.string.plus_tax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Delete", {
|
UIDialogs.Action("Delete", {
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id, true);
|
||||||
_didDelete = true;
|
_didDelete = true;
|
||||||
fragment.close(true);
|
fragment.close(true);
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||||
|
@ -253,7 +253,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
if(g.urls.isEmpty() && g.image == null) {
|
if(g.urls.isEmpty() && g.image == null) {
|
||||||
//Obtain image
|
//Obtain image
|
||||||
for(sub in it) {
|
for(sub in it) {
|
||||||
val sub = StateSubscriptions.instance.getSubscription(sub);
|
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||||
if(sub != null && sub.channel.thumbnail != null) {
|
if(sub != null && sub.channel.thumbnail != null) {
|
||||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||||
g.image?.setImageView(_imageGroup);
|
g.image?.setImageView(_imageGroup);
|
||||||
|
@ -308,8 +308,10 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
|
|
||||||
if(group != null) {
|
if(group != null) {
|
||||||
val urls = group.urls.toList();
|
val urls = group.urls.toList();
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
|
val subs = urls.map {
|
||||||
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
|
(StateSubscriptions.instance.getSubscription(it) ?: StateSubscriptions.instance.getSubscriptionOther(it))?.channel
|
||||||
|
}.filterNotNull();
|
||||||
|
_enabledCreators.addAll(subs);
|
||||||
}
|
}
|
||||||
updateMeta();
|
updateMeta();
|
||||||
filterCreators();
|
filterCreators();
|
||||||
|
|
|
@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.activities.AddSourceOptionsActivity
|
import com.futo.platformplayer.activities.AddSourceOptionsActivity
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
|
@ -57,10 +58,19 @@ class SubscriptionGroupListFragment : MainFragment() {
|
||||||
|
|
||||||
};
|
};
|
||||||
it.onDelete.subscribe { group ->
|
it.onDelete.subscribe { group ->
|
||||||
val loc = _subs.indexOf(group);
|
context?.let { context ->
|
||||||
_subs.remove(group);
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${group.name}]?", null, 0,
|
||||||
_list?.adapter?.notifyItemRangeRemoved(loc);
|
UIDialogs.Action("Cancel", {}),
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
|
UIDialogs.Action("Delete", {
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
|
||||||
|
|
||||||
|
val loc = _subs.indexOf(group);
|
||||||
|
_subs.remove(group);
|
||||||
|
_list?.adapter?.notifyItemRangeRemoved(loc);
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
it.onDragDrop.subscribe {
|
it.onDragDrop.subscribe {
|
||||||
_touchHelper?.startDrag(it);
|
_touchHelper?.startDrag(it);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
|
@ -364,6 +365,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
|
StatePlugins.instance.clearUpdating(); //Fallback in case it doesnt clear, UI should be blocked.
|
||||||
loadResults(true);
|
loadResults(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ class VideoDetailFragment : MainFragment {
|
||||||
val currentRequestedOrientation = a.requestedOrientation
|
val currentRequestedOrientation = a.requestedOrientation
|
||||||
var currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
|
var currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
|
||||||
if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT && !Settings.instance.playback.reversePortrait)
|
if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT && !Settings.instance.playback.reversePortrait)
|
||||||
currentOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
currentOrientation = currentRequestedOrientation
|
||||||
|
|
||||||
val isAutoRotate = Settings.instance.playback.isAutoRotate()
|
val isAutoRotate = Settings.instance.playback.isAutoRotate()
|
||||||
val isFs = isFullscreen
|
val isFs = isFullscreen
|
||||||
|
@ -347,7 +347,7 @@ class VideoDetailFragment : MainFragment {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || (Settings.instance.playback.reversePortrait && _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT))) {
|
||||||
_viewDetail?.setFullscreen(false)
|
_viewDetail?.setFullscreen(false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import androidx.media3.ui.TimeBar
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
@ -72,6 +73,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
@ -111,9 +113,12 @@ import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
|
import com.futo.platformplayer.sync.models.SendToDevicePackage
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSize
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
import com.futo.platformplayer.toHumanNowDiffString
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
|
@ -637,6 +642,27 @@ class VideoDetailView : ConstraintLayout {
|
||||||
StatePlayer.instance.onVideoChanging.subscribe(this) {
|
StatePlayer.instance.onVideoChanging.subscribe(this) {
|
||||||
setVideoOverview(it);
|
setVideoOverview(it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var hadDevice = false;
|
||||||
|
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { id, session ->
|
||||||
|
val hasDevice = StateSync.instance.hasAtLeastOneOnlineDevice();
|
||||||
|
if(hasDevice != hadDevice) {
|
||||||
|
hadDevice = hasDevice;
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
updateMoreButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
StateSync.instance.deviceRemoved.subscribe(this) { id ->
|
||||||
|
val hasDevice = StateSync.instance.hasAtLeastOneOnlineDevice();
|
||||||
|
if(hasDevice != hadDevice) {
|
||||||
|
hadDevice = hasDevice;
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
updateMoreButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
|
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
|
||||||
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
|
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
|
||||||
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
|
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
|
||||||
|
@ -711,6 +737,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
};
|
};
|
||||||
|
|
||||||
onClose.subscribe {
|
onClose.subscribe {
|
||||||
|
checkAndRemoveWatchLater();
|
||||||
_lastVideoSource = null;
|
_lastVideoSource = null;
|
||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
_lastSubtitleSource = null;
|
_lastSubtitleSource = null;
|
||||||
|
@ -819,6 +846,11 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
|
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||||
|
if (it is JSClient)
|
||||||
|
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
||||||
|
else false;
|
||||||
|
} ?: false;
|
||||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||||
(video ?: _searchVideo)?.let {
|
(video ?: _searchVideo)?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
|
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
|
||||||
|
@ -838,38 +870,44 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
} else null,
|
} else null,
|
||||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
if(!isLimitedVersion)
|
||||||
if(!allowBackground) {
|
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||||
_player.switchToAudioMode();
|
if(!allowBackground) {
|
||||||
allowBackground = true;
|
_player.switchToAudioMode();
|
||||||
it.text.text = resources.getString(R.string.background_revert);
|
allowBackground = true;
|
||||||
|
it.text.text = resources.getString(R.string.background_revert);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_player.switchToVideoMode();
|
||||||
|
allowBackground = false;
|
||||||
|
it.text.text = resources.getString(R.string.background);
|
||||||
|
}
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
}
|
}
|
||||||
else {
|
else null,
|
||||||
_player.switchToVideoMode();
|
if(!isLimitedVersion)
|
||||||
allowBackground = false;
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
it.text.text = resources.getString(R.string.background);
|
video?.let {
|
||||||
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
_slideUpOverlay?.hide();
|
else null,
|
||||||
},
|
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
|
||||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
video?.let {
|
||||||
video?.let {
|
Logger.i(TAG, "Share preventPictureInPicture = true");
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
preventPictureInPicture = true;
|
||||||
};
|
shareVideo();
|
||||||
},
|
};
|
||||||
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
|
_slideUpOverlay?.hide();
|
||||||
video?.let {
|
},
|
||||||
Logger.i(TAG, "Share preventPictureInPicture = true");
|
if(!isLimitedVersion)
|
||||||
preventPictureInPicture = true;
|
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
||||||
shareVideo();
|
this.startPictureInPicture();
|
||||||
};
|
fragment.forcePictureInPicture();
|
||||||
_slideUpOverlay?.hide();
|
//PiPActivity.startPiP(context);
|
||||||
},
|
_slideUpOverlay?.hide();
|
||||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
}
|
||||||
this.startPictureInPicture();
|
else null,
|
||||||
fragment.forcePictureInPicture();
|
|
||||||
//PiPActivity.startPiP(context);
|
|
||||||
_slideUpOverlay?.hide();
|
|
||||||
},
|
|
||||||
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||||
video?.let {
|
video?.let {
|
||||||
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
||||||
|
@ -878,6 +916,22 @@ class VideoDetailView : ConstraintLayout {
|
||||||
};
|
};
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
},
|
},
|
||||||
|
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||||
|
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
|
||||||
|
val devices = StateSync.instance.getSessions();
|
||||||
|
val videoToSend = video ?: return@RoundButton;
|
||||||
|
if(devices.size > 1) {
|
||||||
|
//not implemented
|
||||||
|
}
|
||||||
|
else if(devices.size == 1){
|
||||||
|
val device = devices.first();
|
||||||
|
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
device.sendJsonData(GJSyncOpcodes.sendToDevices, SendToDevicePackage(videoToSend.url, (lastPositionMilliseconds/1000).toInt()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}} else null,
|
||||||
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
||||||
reloadVideo();
|
reloadVideo();
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
|
@ -1025,6 +1079,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
StateApp.instance.preventPictureInPicture.remove(this);
|
StateApp.instance.preventPictureInPicture.remove(this);
|
||||||
StatePlayer.instance.onQueueChanged.remove(this);
|
StatePlayer.instance.onQueueChanged.remove(this);
|
||||||
StatePlayer.instance.onVideoChanging.remove(this);
|
StatePlayer.instance.onVideoChanging.remove(this);
|
||||||
|
StateSync.instance.deviceUpdatedOrAdded.remove(this);
|
||||||
|
StateSync.instance.deviceRemoved.remove(this);
|
||||||
MediaControlReceiver.onLowerVolumeReceived.remove(this);
|
MediaControlReceiver.onLowerVolumeReceived.remove(this);
|
||||||
MediaControlReceiver.onPlayReceived.remove(this);
|
MediaControlReceiver.onPlayReceived.remove(this);
|
||||||
MediaControlReceiver.onPauseReceived.remove(this);
|
MediaControlReceiver.onPauseReceived.remove(this);
|
||||||
|
@ -1642,7 +1698,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
_player.setArtwork(null);
|
_player.setArtwork(null);
|
||||||
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
_player.setSource(videoSource, audioSource, _playWhenReady, false, resume = resumePositionMs > 0);
|
||||||
if(subtitleSource != null)
|
if(subtitleSource != null)
|
||||||
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||||
_player.seekTo(resumePositionMs);
|
_player.seekTo(resumePositionMs);
|
||||||
|
@ -1792,6 +1848,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fun prevVideo(withoutRemoval: Boolean = false) {
|
fun prevVideo(withoutRemoval: Boolean = false) {
|
||||||
Logger.i(TAG, "prevVideo")
|
Logger.i(TAG, "prevVideo")
|
||||||
|
checkAndRemoveWatchLater();
|
||||||
|
|
||||||
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
setVideoOverview(next, true, 0, true);
|
setVideoOverview(next, true, 0, true);
|
||||||
|
@ -1800,6 +1858,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
|
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
|
||||||
Logger.i(TAG, "nextVideo")
|
Logger.i(TAG, "nextVideo")
|
||||||
|
checkAndRemoveWatchLater();
|
||||||
|
|
||||||
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
|
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
|
||||||
val autoplayVideo = _autoplayVideo
|
val autoplayVideo = _autoplayVideo
|
||||||
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
|
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
|
||||||
|
@ -1808,7 +1868,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
next = autoplayVideo
|
next = autoplayVideo
|
||||||
}
|
}
|
||||||
_autoplayVideo = null
|
_autoplayVideo = null
|
||||||
Logger.i(TAG, "Autoplay video cleared (nextVideo)")
|
Logger.i(TAG, "Autoplay video cleared (nextVideo)");
|
||||||
|
|
||||||
if(next == null && forceLoop)
|
if(next == null && forceLoop)
|
||||||
next = StatePlayer.instance.restartQueue();
|
next = StatePlayer.instance.restartQueue();
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
|
@ -1820,6 +1881,20 @@ class VideoDetailView : ConstraintLayout {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkAndRemoveWatchLater(){
|
||||||
|
val watchCurrent = video ?: videoLocal ?: _searchVideo;
|
||||||
|
if(Settings.instance.playback.deleteFromWatchLaterAuto) {
|
||||||
|
if(watchCurrent?.duration != null &&
|
||||||
|
watchCurrent.duration > 0 &&
|
||||||
|
(lastPositionMilliseconds / 1000) > watchCurrent.duration * 0.7) {
|
||||||
|
if(!watchCurrent.url.isNullOrEmpty()) {
|
||||||
|
StatePlaylists.instance.removeFromWatchLater(watchCurrent.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//Quality Selector data
|
//Quality Selector data
|
||||||
private fun updateQualityFormatsOverlay(liveStreamVideoFormats : List<Format>?, liveStreamAudioFormats : List<Format>?) {
|
private fun updateQualityFormatsOverlay(liveStreamVideoFormats : List<Format>?, liveStreamAudioFormats : List<Format>?) {
|
||||||
val v = video ?: return;
|
val v = video ?: return;
|
||||||
|
@ -2860,6 +2935,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
const val TAG_OVERLAY = "overlay";
|
const val TAG_OVERLAY = "overlay";
|
||||||
const val TAG_LIVECHAT = "livechat";
|
const val TAG_LIVECHAT = "livechat";
|
||||||
const val TAG_OPEN = "open";
|
const val TAG_OPEN = "open";
|
||||||
|
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||||
const val TAG_MORE = "MORE";
|
const val TAG_MORE = "MORE";
|
||||||
|
|
||||||
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
|
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
|
||||||
|
|
|
@ -191,21 +191,21 @@ class VideoHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun estimateSourceSize(source: IVideoSource?): Int {
|
fun estimateSourceSize(source: IVideoSource?): Long {
|
||||||
if(source == null) return 0;
|
if(source == null) return 0;
|
||||||
if(source is IVideoSource) {
|
if(source is IVideoSource) {
|
||||||
if(source.bitrate ?: 0 <= 0 || source.duration.toInt() == 0)
|
if(source.bitrate ?: 0 <= 0 || source.duration.toInt() == 0)
|
||||||
return 0;
|
return 0;
|
||||||
return (source.duration / 8).toInt() * source.bitrate!!;
|
return (source.duration / 8) * source.bitrate!!;
|
||||||
}
|
}
|
||||||
else return 0;
|
else return 0;
|
||||||
}
|
}
|
||||||
fun estimateSourceSize(source: IAudioSource?): Int {
|
fun estimateSourceSize(source: IAudioSource?): Long {
|
||||||
if(source == null) return 0;
|
if(source == null) return 0;
|
||||||
if(source is IAudioSource) {
|
if(source is IAudioSource) {
|
||||||
if(source.bitrate <= 0 || source.duration?.toInt() ?: 0 == 0)
|
if(source.bitrate <= 0 || source.duration?.toInt() ?: 0 == 0)
|
||||||
return 0;
|
return 0;
|
||||||
return (source.duration!! / 8).toInt() * source.bitrate;
|
return (source.duration!! / 8) * source.bitrate;
|
||||||
}
|
}
|
||||||
else return 0;
|
else return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,10 @@ class MDNSListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
if (_started) throw Exception("Already running.")
|
if (_started) {
|
||||||
|
Logger.i(TAG, "Already started.")
|
||||||
|
return
|
||||||
|
}
|
||||||
_started = true
|
_started = true
|
||||||
|
|
||||||
_scope = CoroutineScope(Dispatchers.IO);
|
_scope = CoroutineScope(Dispatchers.IO);
|
||||||
|
|
|
@ -37,7 +37,10 @@ class ServiceDiscoverer(names: Array<String>, private val _onServicesUpdated: (L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
if (_started) throw Exception("Already running.")
|
if (_started) {
|
||||||
|
Logger.i(TAG, "Already started.")
|
||||||
|
return
|
||||||
|
}
|
||||||
_started = true
|
_started = true
|
||||||
|
|
||||||
val listener = MDNSListener()
|
val listener = MDNSListener()
|
||||||
|
|
|
@ -100,33 +100,34 @@ class ServiceRecordAggregator {
|
||||||
Logger.i(TAG, "$builder")*/
|
Logger.i(TAG, "$builder")*/
|
||||||
|
|
||||||
val currentServices: MutableList<DnsService>
|
val currentServices: MutableList<DnsService>
|
||||||
|
ptrRecords.forEach { record ->
|
||||||
|
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
|
||||||
|
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
|
||||||
|
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
|
||||||
|
}
|
||||||
|
|
||||||
|
aRecords.forEach { aRecord ->
|
||||||
|
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
|
||||||
|
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
|
||||||
|
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
|
||||||
|
}
|
||||||
|
|
||||||
|
aaaaRecords.forEach { aaaaRecord ->
|
||||||
|
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
|
||||||
|
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
|
||||||
|
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
|
||||||
|
}
|
||||||
|
|
||||||
|
txtRecords.forEach { txtRecord ->
|
||||||
|
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
|
||||||
|
}
|
||||||
|
|
||||||
|
srvRecords.forEach { srvRecord ->
|
||||||
|
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Maybe this can be debounced?
|
||||||
synchronized(this._currentServices) {
|
synchronized(this._currentServices) {
|
||||||
ptrRecords.forEach { record ->
|
|
||||||
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
|
|
||||||
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
|
|
||||||
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
|
|
||||||
}
|
|
||||||
|
|
||||||
aRecords.forEach { aRecord ->
|
|
||||||
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
|
|
||||||
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
|
|
||||||
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
|
|
||||||
}
|
|
||||||
|
|
||||||
aaaaRecords.forEach { aaaaRecord ->
|
|
||||||
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
|
|
||||||
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
|
|
||||||
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
|
|
||||||
}
|
|
||||||
|
|
||||||
txtRecords.forEach { txtRecord ->
|
|
||||||
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
|
|
||||||
}
|
|
||||||
|
|
||||||
srvRecords.forEach { srvRecord ->
|
|
||||||
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentServices = getCurrentServices()
|
currentServices = getCurrentServices()
|
||||||
this._currentServices.clear()
|
this._currentServices.clear()
|
||||||
this._currentServices.addAll(currentServices)
|
this._currentServices.addAll(currentServices)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.futo.platformplayer.models
|
package com.futo.platformplayer.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
|
@ -10,6 +12,11 @@ open class SubscriptionGroup {
|
||||||
var urls: MutableList<String> = mutableListOf();
|
var urls: MutableList<String> = mutableListOf();
|
||||||
var priority: Int = 99;
|
var priority: Int = 99;
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastChange : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var creationTime : OffsetDateTime = OffsetDateTime.now();
|
||||||
|
|
||||||
constructor(name: String) {
|
constructor(name: String) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +26,8 @@ open class SubscriptionGroup {
|
||||||
this.image = parent.image;
|
this.image = parent.image;
|
||||||
this.urls = parent.urls;
|
this.urls = parent.urls;
|
||||||
this.priority = parent.priority;
|
this.priority = parent.priority;
|
||||||
|
this.lastChange = parent.lastChange;
|
||||||
|
this.creationTime = parent.creationTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {
|
class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.timestampRegex
|
import com.futo.platformplayer.timestampRegex
|
||||||
|
import com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||||
|
import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
|
@ -23,6 +25,7 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
|
|
||||||
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
|
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
|
||||||
val action = event.action;
|
val action = event.action;
|
||||||
|
Logger.i(TAG, "onTouchEvent (action = $action)")
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
if (action == MotionEvent.ACTION_UP) {
|
||||||
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX;
|
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX;
|
||||||
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY;
|
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY;
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.futo.platformplayer.states
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
@ -20,9 +22,13 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs.Action
|
||||||
|
import com.futo.platformplayer.UIDialogs.ActionStyle
|
||||||
|
import com.futo.platformplayer.UIDialogs.Companion.showDialog
|
||||||
import com.futo.platformplayer.activities.CaptchaActivity
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
|
@ -419,8 +425,17 @@ class StateApp {
|
||||||
Logger.onLogSubmitted.subscribe {
|
Logger.onLogSubmitted.subscribe {
|
||||||
scopeOrNull?.launch(Dispatchers.Main) {
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (it != null) {
|
if (!it.isNullOrEmpty()) {
|
||||||
UIDialogs.toast("Uploaded $it", true);
|
(SettingsActivity.getActivity() ?: contextOrNull)?.let { c ->
|
||||||
|
val okButtonAction = Action(c.getString(R.string.ok), {}, ActionStyle.PRIMARY)
|
||||||
|
val copyButtonAction = Action(c.getString(R.string.copy), {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("Log id", it)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
}, ActionStyle.NONE)
|
||||||
|
|
||||||
|
showDialog(c, R.drawable.ic_error, "Uploaded $it", null, null, 0, copyButtonAction, okButtonAction)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
UIDialogs.toast("Failed to upload");
|
UIDialogs.toast("Failed to upload");
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.copyTo
|
import com.futo.platformplayer.copyTo
|
||||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
||||||
|
@ -18,7 +19,9 @@ import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||||
import com.futo.platformplayer.getNowDiffHours
|
import com.futo.platformplayer.getNowDiffHours
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.readBytes
|
import com.futo.platformplayer.readBytes
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
@ -61,9 +64,9 @@ class StateBackup {
|
||||||
StatePlaylists.instance.toMigrateCheck()
|
StatePlaylists.instance.toMigrateCheck()
|
||||||
).flatten();
|
).flatten();
|
||||||
|
|
||||||
fun getCache(): ImportCache {
|
fun getCache(additionalVideos: List<SerializedPlatformVideo> = listOf()): ImportCache {
|
||||||
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
||||||
val videos = allPlaylists.flatMap { it.videos }.distinctBy { it.url };
|
val videos = allPlaylists.flatMap { it.videos }.plus(additionalVideos).distinctBy { it.url };
|
||||||
|
|
||||||
val allSubscriptions = StateSubscriptions.instance.getSubscriptions();
|
val allSubscriptions = StateSubscriptions.instance.getSubscriptions();
|
||||||
val channels = allSubscriptions.map { it.channel };
|
val channels = allSubscriptions.map { it.channel };
|
||||||
|
@ -240,6 +243,23 @@ class StateBackup {
|
||||||
.associateBy { it.name }
|
.associateBy { it.name }
|
||||||
.mapValues { it.value.getAllReconstructionStrings() }
|
.mapValues { it.value.getAllReconstructionStrings() }
|
||||||
.toMutableMap();
|
.toMutableMap();
|
||||||
|
|
||||||
|
var historyVideos: List<SerializedPlatformVideo>? = null;
|
||||||
|
try {
|
||||||
|
storesToSave.set("subscription_groups", StateSubscriptionGroups.instance.getSubscriptionGroups().map { Json.encodeToString(it) });
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to serialize subscription groups");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val history = StateHistory.instance.getRecentHistory(OffsetDateTime.MIN, 2000);
|
||||||
|
historyVideos = history.map { it.video };
|
||||||
|
storesToSave.set("history", history.map { it.toReconString() });
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to serialize history");
|
||||||
|
}
|
||||||
|
|
||||||
val settings = Settings.instance.encode();
|
val settings = Settings.instance.encode();
|
||||||
val pluginSettings = StatePlugins.instance.getPlugins()
|
val pluginSettings = StatePlugins.instance.getPlugins()
|
||||||
.associateBy { it.config.id }
|
.associateBy { it.config.id }
|
||||||
|
@ -249,7 +269,7 @@ class StateBackup {
|
||||||
.associateBy { it.config.id }
|
.associateBy { it.config.id }
|
||||||
.mapValues { it.value.config.sourceUrl!! };
|
.mapValues { it.value.config.sourceUrl!! };
|
||||||
|
|
||||||
val cache = getCache();
|
val cache = getCache(historyVideos ?: listOf());
|
||||||
|
|
||||||
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
|
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
|
||||||
|
|
||||||
|
@ -333,19 +353,64 @@ class StateBackup {
|
||||||
if(doImportStores) {
|
if(doImportStores) {
|
||||||
for(store in export.stores) {
|
for(store in export.stores) {
|
||||||
Logger.i(TAG, "Importing store [${store.key}]");
|
Logger.i(TAG, "Importing store [${store.key}]");
|
||||||
val relevantStore = availableStores.find { it.name == store.key };
|
if(store.key == "history") {
|
||||||
if(relevantStore == null) {
|
withContext(Dispatchers.Main) {
|
||||||
Logger.w(TAG, "Unknown store [${store.key}] import");
|
UIDialogs.showDialog(context, R.drawable.ic_move_up, "Import History", "Would you like to import history?", null, 0,
|
||||||
continue;
|
UIDialogs.Action("No", {
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Yes", {
|
||||||
|
for(historyStr in store.value) {
|
||||||
|
try {
|
||||||
|
val histObj = HistoryVideo.fromReconString(historyStr) { url ->
|
||||||
|
return@fromReconString export.cache?.videos?.firstOrNull { it.url == url };
|
||||||
|
}
|
||||||
|
val hist = StateHistory.instance.getHistoryByVideo(histObj.video, true, histObj.date);
|
||||||
|
if(hist != null)
|
||||||
|
StateHistory.instance.updateHistoryPosition(histObj.video, hist, true, histObj.position, histObj.date, false);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to import subscription group", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
else if(store.key == "subscription_groups") {
|
||||||
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
|
withContext(Dispatchers.Main) {
|
||||||
synchronized(toAwait) {
|
UIDialogs.showDialog(context, R.drawable.ic_move_up, "Import Subscription Groups", "Would you like to import subscription groups?\nExisting groups with the same id will be overridden!", null, 0,
|
||||||
toAwait.remove(store.key);
|
UIDialogs.Action("No", {
|
||||||
if(toAwait.isEmpty())
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
onConclusion();
|
UIDialogs.Action("Yes", {
|
||||||
}
|
for(groupStr in store.value) {
|
||||||
};
|
try {
|
||||||
|
val group = Json.decodeFromString<SubscriptionGroup>(groupStr);
|
||||||
|
val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
|
||||||
|
if(existing != null)
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(existing.id, false);
|
||||||
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(group);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to import subscription group", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val relevantStore = availableStores.find { it.name == store.key };
|
||||||
|
if (relevantStore == null) {
|
||||||
|
Logger.w(TAG, "Unknown store [${store.key}] import");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
|
||||||
|
synchronized(toAwait) {
|
||||||
|
toAwait.remove(store.key);
|
||||||
|
if(toAwait.isEmpty())
|
||||||
|
onConclusion();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,6 @@ class StateHistory {
|
||||||
return getHistoryPosition(url) > duration * 0.7;
|
return getHistoryPosition(url) > duration * 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false): Long {
|
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false): Long {
|
||||||
val pos = if(position < 0) 0 else position;
|
val pos = if(position < 0) 0 else position;
|
||||||
val historyVideo = index.obj;
|
val historyVideo = index.obj;
|
||||||
|
@ -90,7 +89,7 @@ class StateHistory {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||||
Logger.i(TAG, "SyncHistory playback broadcasted (${liveObj.name}: ${position})");
|
Logger.i(TAG, "SyncHistory playback broadcasted (${liveObj.name}: ${position})");
|
||||||
StateSync.instance.broadcastJson(
|
StateSync.instance.broadcastJsonData(
|
||||||
GJSyncOpcodes.syncHistory,
|
GJSyncOpcodes.syncHistory,
|
||||||
listOf(historyVideo)
|
listOf(historyVideo)
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,10 +17,18 @@ import com.futo.platformplayer.exceptions.ReconstructionException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
|
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
|
||||||
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
|
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -45,6 +53,7 @@ class StatePlaylists {
|
||||||
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
||||||
.withRestore(PlaylistBackup())
|
.withRestore(PlaylistBackup())
|
||||||
.load();
|
.load();
|
||||||
|
private val _playlistRemoved = FragmentedStorage.get<StringDateMapStorage>("playlist_removed");
|
||||||
|
|
||||||
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
|
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
|
||||||
|
|
||||||
|
@ -81,6 +90,18 @@ class StatePlaylists {
|
||||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun getWatchLaterFromUrl(url: String): SerializedPlatformVideo?{
|
||||||
|
synchronized(_watchlistStore) {
|
||||||
|
val order = _watchlistOrderStore.getAllValues();
|
||||||
|
return _watchlistStore.getItems().firstOrNull { it.url == url };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun removeFromWatchLater(url: String) {
|
||||||
|
val item = getWatchLaterFromUrl(url);
|
||||||
|
if(item != null){
|
||||||
|
removeFromWatchLater(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
_watchlistStore.delete(video);
|
_watchlistStore.delete(video);
|
||||||
|
@ -118,6 +139,9 @@ class StatePlaylists {
|
||||||
return playlistStore.findItem { it.id == id };
|
return playlistStore.findItem { it.id == id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPlaylistRemovals(): Map<String, Long> {
|
||||||
|
return _playlistRemoved.all();
|
||||||
|
}
|
||||||
|
|
||||||
fun didPlay(playlistId: String) {
|
fun didPlay(playlistId: String) {
|
||||||
val playlist = getPlaylist(playlistId);
|
val playlist = getPlaylist(playlistId);
|
||||||
|
@ -148,13 +172,15 @@ class StatePlaylists {
|
||||||
createOrUpdatePlaylist(newPlaylist);
|
createOrUpdatePlaylist(newPlaylist);
|
||||||
return newPlaylist;
|
return newPlaylist;
|
||||||
}
|
}
|
||||||
fun createOrUpdatePlaylist(playlist: Playlist) {
|
fun createOrUpdatePlaylist(playlist: Playlist, isUserInteraction: Boolean = true) {
|
||||||
playlist.dateUpdate = OffsetDateTime.now();
|
playlist.dateUpdate = OffsetDateTime.now();
|
||||||
playlistStore.saveAsync(playlist, true);
|
playlistStore.saveAsync(playlist, true);
|
||||||
if(playlist.id.isNotEmpty()) {
|
if(playlist.id.isNotEmpty()) {
|
||||||
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
|
||||||
}
|
}
|
||||||
|
if(isUserInteraction)
|
||||||
|
broadcastSyncPlaylist(playlist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
||||||
|
@ -163,14 +189,41 @@ class StatePlaylists {
|
||||||
playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
|
playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
|
||||||
playlist.dateUpdate = OffsetDateTime.now();
|
playlist.dateUpdate = OffsetDateTime.now();
|
||||||
playlistStore.saveAsync(playlist, true);
|
playlistStore.saveAsync(playlist, true);
|
||||||
|
|
||||||
|
broadcastSyncPlaylist(playlist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePlaylist(playlist: Playlist) {
|
private fun broadcastSyncPlaylist(playlist: Playlist){
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||||
|
Logger.i(StateSubscriptionGroups.TAG, "SyncPlaylist (${playlist.name})");
|
||||||
|
StateSync.instance.broadcastJsonData(
|
||||||
|
GJSyncOpcodes.syncPlaylists,
|
||||||
|
SyncPlaylistsPackage(listOf(playlist), mapOf())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePlaylist(playlist: Playlist, isUserInteraction: Boolean = true) {
|
||||||
playlistStore.delete(playlist);
|
playlistStore.delete(playlist);
|
||||||
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||||
}
|
}
|
||||||
|
if(isUserInteraction) {
|
||||||
|
_playlistRemoved.setAndSave(playlist.id, OffsetDateTime.now());
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||||
|
Logger.i(StateSubscriptionGroups.TAG, "SyncPlaylist (${playlist.name})");
|
||||||
|
StateSync.instance.broadcastJsonData(
|
||||||
|
GJSyncOpcodes.syncPlaylists,
|
||||||
|
SyncPlaylistsPackage(listOf(), mapOf(Pair(playlist.id, OffsetDateTime.now().toEpochSecond())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
|
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
|
||||||
|
@ -194,6 +247,16 @@ class StatePlaylists {
|
||||||
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
|
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getSyncPlaylistsPackageString(): String{
|
||||||
|
return Json.encodeToString(
|
||||||
|
SyncPlaylistsPackage(
|
||||||
|
getPlaylists(),
|
||||||
|
getPlaylistRemovals()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "StatePlaylists";
|
val TAG = "StatePlaylists";
|
||||||
private var _instance : StatePlaylists? = null;
|
private var _instance : StatePlaylists? = null;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import com.futo.platformplayer.stores.PluginIconStorage
|
||||||
import com.futo.platformplayer.stores.PluginScriptsDirectory
|
import com.futo.platformplayer.stores.PluginScriptsDirectory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
@ -47,6 +48,8 @@ class StatePlugins {
|
||||||
|
|
||||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||||
|
|
||||||
|
private val _isUpdating: HashSet<String> = hashSetOf();
|
||||||
|
|
||||||
fun getPluginIconOrNull(id: String): ImageVariable? {
|
fun getPluginIconOrNull(id: String): ImageVariable? {
|
||||||
if(iconsDir.hasIcon(id))
|
if(iconsDir.hasIcon(id))
|
||||||
return iconsDir.getIconBinary(id);
|
return iconsDir.getIconBinary(id);
|
||||||
|
@ -58,6 +61,38 @@ class StatePlugins {
|
||||||
.load();
|
.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isUpdating(id: String): Boolean{
|
||||||
|
synchronized(_isUpdating){
|
||||||
|
return _isUpdating.contains(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun setIsUpdating(id: String, value: Boolean){
|
||||||
|
synchronized(_isUpdating){
|
||||||
|
if(value && !_isUpdating.contains(id)) {
|
||||||
|
Logger.i(TAG, "PLUGIN [${id}] UPDATING");
|
||||||
|
_isUpdating.add(id);
|
||||||
|
}
|
||||||
|
if(!value && _isUpdating.contains(id)) {
|
||||||
|
Logger.i(TAG, "PLUGIN [${id}] NOT UPDATING");
|
||||||
|
_isUpdating.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suspend fun whileUpdating(id: String, handle: suspend ()->Unit){
|
||||||
|
try {
|
||||||
|
setIsUpdating(id, true);
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsUpdating(id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun clearUpdating(){
|
||||||
|
synchronized(_isUpdating) {
|
||||||
|
_isUpdating.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun checkForUpdates(): List<Pair<SourcePluginConfig, SourcePluginConfig>> = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdates(): List<Pair<SourcePluginConfig, SourcePluginConfig>> = withContext(Dispatchers.IO) {
|
||||||
var configs = mutableListOf<Pair<SourcePluginConfig, SourcePluginConfig>>()
|
var configs = mutableListOf<Pair<SourcePluginConfig, SourcePluginConfig>>()
|
||||||
|
@ -430,42 +465,49 @@ class StatePlugins {
|
||||||
|
|
||||||
fun installPluginBackground(context: Context, scope: CoroutineScope, config: SourcePluginConfig, script: String, onProgress: (text: String, progress: Double)->Unit, onConcluded: (ex: Throwable?)->Unit) {
|
fun installPluginBackground(context: Context, scope: CoroutineScope, config: SourcePluginConfig, script: String, onProgress: (text: String, progress: Double)->Unit, onConcluded: (ex: Throwable?)->Unit) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val client = ManagedHttpClient();
|
whileUpdating(config.id) {
|
||||||
try {
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
onProgress.invoke("Validating script", 0.25);
|
onProgress.invoke("Waiting for plugins to finish", 0.1);
|
||||||
}
|
}
|
||||||
|
delay(500);
|
||||||
|
|
||||||
val tempDescriptor = SourcePluginDescriptor(config);
|
val client = ManagedHttpClient();
|
||||||
val plugin = JSClient(context, tempDescriptor, null, script);
|
try {
|
||||||
plugin.validate();
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onProgress.invoke("Downloading Icon", 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
onProgress.invoke("Saving plugin", 0.75);
|
onProgress.invoke("Validating script", 0.25);
|
||||||
}
|
}
|
||||||
val iconResp = client.get(absIconUrl);
|
|
||||||
if(iconResp.isOk)
|
|
||||||
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
|
||||||
return@let null;
|
|
||||||
}
|
|
||||||
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
|
||||||
if(installEx != null)
|
|
||||||
throw installEx;
|
|
||||||
StatePlatform.instance.updateAvailableClients(context);
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
val tempDescriptor = SourcePluginDescriptor(config);
|
||||||
onProgress.invoke("Finished", 1.0)
|
val plugin = JSClient(context, tempDescriptor, null, script);
|
||||||
onConcluded.invoke(null);
|
plugin.validate();
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
withContext(Dispatchers.Main) {
|
||||||
Logger.e(TAG, ex.message ?: "null", ex);
|
onProgress.invoke("Downloading Icon", 0.5);
|
||||||
withContext(Dispatchers.Main) {
|
}
|
||||||
onConcluded.invoke(ex);
|
|
||||||
|
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Saving plugin", 0.75);
|
||||||
|
}
|
||||||
|
val iconResp = client.get(absIconUrl);
|
||||||
|
if (iconResp.isOk)
|
||||||
|
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||||
|
return@let null;
|
||||||
|
}
|
||||||
|
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
||||||
|
if (installEx != null)
|
||||||
|
throw installEx;
|
||||||
|
StatePlatform.instance.updateAvailableClients(context);
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Finished", 1.0)
|
||||||
|
onConcluded.invoke(null);
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e(TAG, ex.message ?: "null", ex);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onConcluded.invoke(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,20 @@ import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
|
import com.futo.platformplayer.states.StateHistory.Companion
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||||
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
|
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
@ -51,6 +58,9 @@ class StateSubscriptionGroups {
|
||||||
.withUnique { it.id }
|
.withUnique { it.id }
|
||||||
.load();
|
.load();
|
||||||
|
|
||||||
|
|
||||||
|
private val _groupsRemoved = FragmentedStorage.get<StringDateMapStorage>("group_removed");
|
||||||
|
|
||||||
val onGroupsChanged = Event0();
|
val onGroupsChanged = Event0();
|
||||||
|
|
||||||
fun getSubscriptionGroup(id: String): SubscriptionGroup? {
|
fun getSubscriptionGroup(id: String): SubscriptionGroup? {
|
||||||
|
@ -59,19 +69,66 @@ class StateSubscriptionGroups {
|
||||||
fun getSubscriptionGroups(): List<SubscriptionGroup> {
|
fun getSubscriptionGroups(): List<SubscriptionGroup> {
|
||||||
return _subGroups.getItems();
|
return _subGroups.getItems();
|
||||||
}
|
}
|
||||||
fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) {
|
fun getSubscriptionGroupsRemovals(): Map<String, Long> {
|
||||||
|
return _groupsRemoved.all();
|
||||||
|
}
|
||||||
|
fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false, preventSync: Boolean = false) {
|
||||||
|
subGroup.lastChange = OffsetDateTime.now();
|
||||||
_subGroups.save(subGroup);
|
_subGroups.save(subGroup);
|
||||||
if(!preventNotify)
|
if(!preventNotify)
|
||||||
onGroupsChanged.emit();
|
onGroupsChanged.emit();
|
||||||
|
if(!preventSync) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||||
|
Logger.i(TAG, "SyncSubscriptionGroup (${subGroup.name})");
|
||||||
|
StateSync.instance.broadcastJsonData(
|
||||||
|
GJSyncOpcodes.syncSubscriptionGroups,
|
||||||
|
SyncSubscriptionGroupsPackage(listOf(subGroup), mapOf())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun deleteSubscriptionGroup(id: String){
|
fun deleteSubscriptionGroup(id: String, isUserInteraction: Boolean = true){
|
||||||
val group = getSubscriptionGroup(id);
|
val group = getSubscriptionGroup(id);
|
||||||
if(group != null) {
|
if(group != null) {
|
||||||
_subGroups.delete(group);
|
_subGroups.delete(group);
|
||||||
onGroupsChanged.emit();
|
onGroupsChanged.emit();
|
||||||
|
|
||||||
|
if(isUserInteraction) {
|
||||||
|
_groupsRemoved.setAndSave(id, OffsetDateTime.now());
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||||
|
Logger.i(TAG, "SyncSubscriptionGroup delete (${group.name})");
|
||||||
|
StateSync.instance.broadcastJsonData(
|
||||||
|
GJSyncOpcodes.syncSubscriptionGroups,
|
||||||
|
SyncSubscriptionGroupsPackage(listOf(), mapOf(Pair(id, OffsetDateTime.now().toEpochSecond())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasSubscriptionGroup(url: String): Boolean {
|
||||||
|
val groups = getSubscriptionGroups();
|
||||||
|
for(group in groups){
|
||||||
|
if(group.urls.contains(url))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getSyncSubscriptionGroupsPackageString(): String{
|
||||||
|
return Json.encodeToString(
|
||||||
|
SyncSubscriptionGroupsPackage(
|
||||||
|
getSubscriptionGroups(),
|
||||||
|
getSubscriptionGroupsRemovals()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "StateSubscriptionGroups";
|
const val TAG = "StateSubscriptionGroups";
|
||||||
|
|
|
@ -202,13 +202,13 @@ class StateSubscriptions {
|
||||||
return _subscriptionOthers.findItem { it.isChannel(url)};
|
return _subscriptionOthers.findItem { it.isChannel(url)};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun getSubscriptionOtherOrCreate(url: String) : Subscription {
|
fun getSubscriptionOtherOrCreate(url: String, name: String? = null, thumbnail: String? = null) : Subscription {
|
||||||
synchronized(_subscriptionOthers) {
|
synchronized(_subscriptionOthers) {
|
||||||
val sub = getSubscriptionOther(url);
|
val sub = getSubscriptionOther(url);
|
||||||
if(sub == null) {
|
if(sub == null) {
|
||||||
val newSub = Subscription(SerializedChannel(PlatformID.NONE, url, null, null, 0, null, url, mapOf()));
|
val newSub = Subscription(SerializedChannel(PlatformID.NONE, name ?: url, thumbnail, null, 0, null, url, mapOf()));
|
||||||
newSub.isOther = true;
|
newSub.isOther = true;
|
||||||
_subscriptions.save(newSub);
|
_subscriptionOthers.save(newSub);
|
||||||
return newSub;
|
return newSub;
|
||||||
}
|
}
|
||||||
else return sub;
|
else return sub;
|
||||||
|
@ -250,7 +250,7 @@ class StateSubscriptions {
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.broadcast(
|
StateSync.instance.broadcastData(
|
||||||
GJSyncOpcodes.syncSubscriptions, Json.encodeToString(
|
GJSyncOpcodes.syncSubscriptions, Json.encodeToString(
|
||||||
SyncSubscriptionsPackage(
|
SyncSubscriptionsPackage(
|
||||||
listOf(subObj),
|
listOf(subObj),
|
||||||
|
@ -293,8 +293,29 @@ class StateSubscriptions {
|
||||||
if(sub != null) {
|
if(sub != null) {
|
||||||
_subscriptions.delete(sub);
|
_subscriptions.delete(sub);
|
||||||
onSubscriptionsChanged.emit(getSubscriptions(), false);
|
onSubscriptionsChanged.emit(getSubscriptions(), false);
|
||||||
if(isUserAction)
|
if(isUserAction) {
|
||||||
_subscriptionsRemoved.setAndSave(sub.channel.url, OffsetDateTime.now());
|
val removalTime = OffsetDateTime.now();
|
||||||
|
_subscriptionsRemoved.setAndSave(sub.channel.url, removalTime);
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
StateSync.instance.broadcastData(
|
||||||
|
GJSyncOpcodes.syncSubscriptions, Json.encodeToString(
|
||||||
|
SyncSubscriptionsPackage(
|
||||||
|
listOf(),
|
||||||
|
mapOf(Pair(sub.channel.url, removalTime.toEpochSecond()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to send subs changes to sync clients", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url))
|
||||||
|
getSubscriptionOtherOrCreate(sub.channel.url, sub.channel.name, sub.channel.thumbnail);
|
||||||
}
|
}
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,10 @@ class StateSync {
|
||||||
val deviceUpdatedOrAdded: Event2<String, SyncSession> = Event2()
|
val deviceUpdatedOrAdded: Event2<String, SyncSession> = Event2()
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
if (_started) {
|
||||||
|
Logger.i(TAG, "Already started.")
|
||||||
|
return
|
||||||
|
}
|
||||||
_started = true
|
_started = true
|
||||||
|
|
||||||
if (Settings.instance.synchronization.broadcast || Settings.instance.synchronization.connectDiscovered) {
|
if (Settings.instance.synchronization.broadcast || Settings.instance.synchronization.connectDiscovered) {
|
||||||
|
@ -366,26 +370,29 @@ class StateSync {
|
||||||
Logger.i(TAG, "Connection authorized for ${remotePublicKey} because initiator")
|
Logger.i(TAG, "Connection authorized for ${remotePublicKey} because initiator")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onData = { s, opcode, data ->
|
onData = { s, opcode, subOpcode, data ->
|
||||||
session?.handlePacket(s, opcode, data)
|
session?.handlePacket(s, opcode, subOpcode, data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> broadcastJson(opcode: UByte, data: T) {
|
inline fun <reified T> broadcastJsonData(subOpcode: UByte, data: T) {
|
||||||
broadcast(opcode, Json.encodeToString(data));
|
broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, Json.encodeToString(data));
|
||||||
}
|
}
|
||||||
fun broadcast(opcode: UByte, data: String) {
|
fun broadcastData(subOpcode: UByte, data: String) {
|
||||||
broadcast(opcode, data.toByteArray(Charsets.UTF_8));
|
broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, data.toByteArray(Charsets.UTF_8));
|
||||||
}
|
}
|
||||||
fun broadcast(opcode: UByte, data: ByteArray) {
|
fun broadcast(opcode: UByte, subOpcode: UByte, data: String) {
|
||||||
|
broadcast(opcode, subOpcode, data.toByteArray(Charsets.UTF_8));
|
||||||
|
}
|
||||||
|
fun broadcast(opcode: UByte, subOpcode: UByte, data: ByteArray) {
|
||||||
for(session in getSessions()) {
|
for(session in getSessions()) {
|
||||||
try {
|
try {
|
||||||
if (session.isAuthorized && session.connected) {
|
if (session.isAuthorized && session.connected) {
|
||||||
session.send(opcode, data);
|
session.send(opcode, subOpcode, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
Logger.w(TAG, "Failed to broadcast ${opcode} to ${session.remotePublicKey}: ${ex.message}}", ex);
|
Logger.w(TAG, "Failed to broadcast (opcode = ${opcode}, subOpcode = ${subOpcode}) to ${session.remotePublicKey}: ${ex.message}}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -394,7 +401,7 @@ class StateSync {
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
//val export = StateBackup.export();
|
//val export = StateBackup.export();
|
||||||
//session.send(GJSyncOpcodes.syncExport, export.asZip());
|
//session.send(GJSyncOpcodes.syncExport, export.asZip());
|
||||||
session.send(GJSyncOpcodes.syncStateExchange, getSyncSessionDataString(session.remotePublicKey));
|
session.sendData(GJSyncOpcodes.syncStateExchange, getSyncSessionDataString(session.remotePublicKey));
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Generated and sent sync export in ${time}ms");
|
Logger.i(TAG, "Generated and sent sync export in ${time}ms");
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
@ -138,6 +139,18 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
|
|
||||||
for(task in tasks) {
|
for(task in tasks) {
|
||||||
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
||||||
|
if(StatePlugins.instance.isUpdating(task.client.id)){
|
||||||
|
val isUpdatingException = ScriptCriticalException(task.client.config, "Plugin is updating");
|
||||||
|
synchronized(failedPlugins) {
|
||||||
|
//Fail all subscription calls to plugin if it has a critical issue
|
||||||
|
if(isUpdatingException.config is SourcePluginConfig && !failedPlugins.contains(isUpdatingException.config.id)) {
|
||||||
|
Logger.w(StateSubscriptions.TAG, "Subscriptions ignoring plugin [${isUpdatingException.config.name}] due to critical exception:\n" + isUpdatingException.message);
|
||||||
|
failedPlugins.add(isUpdatingException.config.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@submit SubscriptionTaskResult(task, StateCache.instance.getChannelCachePager(task.sub.channel.url), isUpdatingException);
|
||||||
|
}
|
||||||
|
|
||||||
if(task.fromPeek) {
|
if(task.fromPeek) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
|
@ -11,5 +11,7 @@ class GJSyncOpcodes {
|
||||||
val syncSubscriptions: UByte = 202.toUByte();
|
val syncSubscriptions: UByte = 202.toUByte();
|
||||||
|
|
||||||
val syncHistory: UByte = 203.toUByte();
|
val syncHistory: UByte = 203.toUByte();
|
||||||
|
val syncSubscriptionGroups: UByte = 204.toUByte();
|
||||||
|
val syncPlaylists: UByte = 205.toUByte();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,15 +6,20 @@ import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.states.StateSync
|
import com.futo.platformplayer.states.StateSync
|
||||||
import com.futo.platformplayer.sync.SyncSessionData
|
import com.futo.platformplayer.sync.SyncSessionData
|
||||||
import com.futo.platformplayer.sync.internal.SyncSocketSession.Opcode
|
import com.futo.platformplayer.sync.internal.SyncSocketSession.Opcode
|
||||||
import com.futo.platformplayer.sync.models.SendToDevicePackage
|
import com.futo.platformplayer.sync.models.SendToDevicePackage
|
||||||
|
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
|
||||||
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -22,7 +27,9 @@ import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
interface IAuthorizable {
|
interface IAuthorizable {
|
||||||
val isAuthorized: Boolean
|
val isAuthorized: Boolean
|
||||||
|
@ -38,6 +45,7 @@ class SyncSession : IAuthorizable {
|
||||||
private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit
|
private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit
|
||||||
val remotePublicKey: String
|
val remotePublicKey: String
|
||||||
override val isAuthorized get() = _authorized && _remoteAuthorized
|
override val isAuthorized get() = _authorized && _remoteAuthorized
|
||||||
|
private var _wasAuthorized = false
|
||||||
|
|
||||||
var connected: Boolean = false
|
var connected: Boolean = false
|
||||||
private set(v) {
|
private set(v) {
|
||||||
|
@ -87,8 +95,10 @@ class SyncSession : IAuthorizable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAuthorized() {
|
private fun checkAuthorized() {
|
||||||
if (isAuthorized)
|
if (!_wasAuthorized && isAuthorized) {
|
||||||
|
_wasAuthorized = true
|
||||||
_onAuthorized.invoke(this)
|
_onAuthorized.invoke(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSocketSession(socketSession: SyncSocketSession) {
|
fun removeSocketSession(socketSession: SyncSocketSession) {
|
||||||
|
@ -110,29 +120,34 @@ class SyncSession : IAuthorizable {
|
||||||
_onClose.invoke(this)
|
_onClose.invoke(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handlePacket(socketSession: SyncSocketSession, opcode: UByte, data: ByteBuffer) {
|
fun handlePacket(socketSession: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
||||||
Logger.i(TAG, "Handle packet (opcode: ${opcode}, data.length: ${data.remaining()})")
|
|
||||||
|
|
||||||
when (opcode) {
|
|
||||||
Opcode.NOTIFY_AUTHORIZED.value -> {
|
|
||||||
_remoteAuthorized = true
|
|
||||||
checkAuthorized()
|
|
||||||
}
|
|
||||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
|
||||||
_remoteAuthorized = false
|
|
||||||
_onUnauthorized(this)
|
|
||||||
}
|
|
||||||
//TODO: Handle any kind of packet (that is not necessarily authorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAuthorized) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Received ${opcode} (${data.remaining()} bytes)")
|
|
||||||
//TODO: Abstract this out
|
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "Handle packet (opcode: ${opcode}, subOpcode: ${subOpcode}, data.length: ${data.remaining()})")
|
||||||
|
|
||||||
when (opcode) {
|
when (opcode) {
|
||||||
|
Opcode.NOTIFY_AUTHORIZED.value -> {
|
||||||
|
_remoteAuthorized = true
|
||||||
|
checkAuthorized()
|
||||||
|
}
|
||||||
|
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||||
|
_remoteAuthorized = false
|
||||||
|
_onUnauthorized(this)
|
||||||
|
}
|
||||||
|
//TODO: Handle any kind of packet (that is not necessarily authorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthorized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opcode != Opcode.DATA.value) {
|
||||||
|
Logger.w(TAG, "Unknown opcode received: (opcode = ${opcode}, subOpcode = ${subOpcode})}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Received (opcode = ${opcode}, subOpcode = ${subOpcode}) (${data.remaining()} bytes)")
|
||||||
|
//TODO: Abstract this out
|
||||||
|
when (subOpcode) {
|
||||||
GJSyncOpcodes.sendToDevices -> {
|
GJSyncOpcodes.sendToDevices -> {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
val context = StateApp.instance.contextOrNull;
|
val context = StateApp.instance.contextOrNull;
|
||||||
|
@ -157,11 +172,13 @@ class SyncSession : IAuthorizable {
|
||||||
Logger.i(TAG, "Received SyncSessionData from " + remotePublicKey);
|
Logger.i(TAG, "Received SyncSessionData from " + remotePublicKey);
|
||||||
|
|
||||||
|
|
||||||
send(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString());
|
sendData(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString());
|
||||||
|
sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
|
||||||
|
sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
|
||||||
|
|
||||||
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
||||||
if(recentHistory.size > 0)
|
if(recentHistory.size > 0)
|
||||||
sendJson(GJSyncOpcodes.syncHistory, recentHistory);
|
sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
GJSyncOpcodes.syncExport -> {
|
GJSyncOpcodes.syncExport -> {
|
||||||
|
@ -205,6 +222,67 @@ class SyncSession : IAuthorizable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GJSyncOpcodes.syncSubscriptionGroups -> {
|
||||||
|
val dataBody = ByteArray(data.remaining());
|
||||||
|
data.get(dataBody);
|
||||||
|
val json = String(dataBody, Charsets.UTF_8);
|
||||||
|
val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json);
|
||||||
|
|
||||||
|
var lastSubgroupChange = OffsetDateTime.MIN;
|
||||||
|
for(group in pack.groups){
|
||||||
|
if(group.lastChange > lastSubgroupChange)
|
||||||
|
lastSubgroupChange = group.lastChange;
|
||||||
|
|
||||||
|
val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
|
||||||
|
|
||||||
|
if(existing == null)
|
||||||
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true);
|
||||||
|
else if(existing.lastChange < group.lastChange) {
|
||||||
|
existing.name = group.name;
|
||||||
|
existing.urls = group.urls;
|
||||||
|
existing.image = group.image;
|
||||||
|
existing.priority = group.priority;
|
||||||
|
existing.lastChange = group.lastChange;
|
||||||
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(removal in pack.groupRemovals) {
|
||||||
|
val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key);
|
||||||
|
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
|
||||||
|
if(creation != null && creation.creationTime < removalTime)
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(removal.key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GJSyncOpcodes.syncPlaylists -> {
|
||||||
|
val dataBody = ByteArray(data.remaining());
|
||||||
|
data.get(dataBody);
|
||||||
|
val json = String(dataBody, Charsets.UTF_8);
|
||||||
|
val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json);
|
||||||
|
|
||||||
|
for(playlist in pack.playlists) {
|
||||||
|
val existing = StatePlaylists.instance.getPlaylist(playlist.id);
|
||||||
|
|
||||||
|
if(existing == null)
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
|
||||||
|
else if(existing.dateUpdate.toLocalDateTime() < playlist.dateUpdate.toLocalDateTime()) {
|
||||||
|
existing.dateUpdate = playlist.dateUpdate;
|
||||||
|
existing.name = playlist.name;
|
||||||
|
existing.videos = playlist.videos;
|
||||||
|
existing.dateCreation = playlist.dateCreation;
|
||||||
|
existing.datePlayed = playlist.datePlayed;
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(removal in pack.playlistRemovals) {
|
||||||
|
val creation = StatePlaylists.instance.getPlaylist(removal.key);
|
||||||
|
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
|
||||||
|
if(creation != null && creation.dateCreation < removalTime)
|
||||||
|
StatePlaylists.instance.removePlaylist(creation, false);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GJSyncOpcodes.syncHistory -> {
|
GJSyncOpcodes.syncHistory -> {
|
||||||
val dataBody = ByteArray(data.remaining());
|
val dataBody = ByteArray(data.remaining());
|
||||||
data.get(dataBody);
|
data.get(dataBody);
|
||||||
|
@ -242,8 +320,7 @@ class SyncSession : IAuthorizable {
|
||||||
if(!StateSubscriptions.instance.isSubscribed(sub.channel)) {
|
if(!StateSubscriptions.instance.isSubscribed(sub.channel)) {
|
||||||
val removalTime = StateSubscriptions.instance.getSubscriptionRemovalTime(sub.channel.url);
|
val removalTime = StateSubscriptions.instance.getSubscriptionRemovalTime(sub.channel.url);
|
||||||
if(sub.creationTime > removalTime) {
|
if(sub.creationTime > removalTime) {
|
||||||
val newSub =
|
val newSub = StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
|
||||||
StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
|
|
||||||
added.add(newSub);
|
added.add(newSub);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,16 +346,19 @@ class SyncSession : IAuthorizable {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
inline fun <reified T> sendJson(opcode: UByte, data: T) {
|
inline fun <reified T> sendJsonData(subOpcode: UByte, data: T) {
|
||||||
send(opcode, Json.encodeToString<T>(data));
|
send(Opcode.DATA.value, subOpcode, Json.encodeToString<T>(data));
|
||||||
}
|
}
|
||||||
fun send(opcode: UByte, data: String) {
|
fun sendData(subOpcode: UByte, data: String) {
|
||||||
send(opcode, data.toByteArray(Charsets.UTF_8));
|
send(Opcode.DATA.value, subOpcode, data.toByteArray(Charsets.UTF_8));
|
||||||
}
|
}
|
||||||
fun send(opcode: UByte, data: ByteArray) {
|
fun send(opcode: UByte, subOpcode: UByte, data: String) {
|
||||||
|
send(opcode, subOpcode, data.toByteArray(Charsets.UTF_8));
|
||||||
|
}
|
||||||
|
fun send(opcode: UByte, subOpcode: UByte, data: ByteArray) {
|
||||||
val sock = _socketSessions.firstOrNull();
|
val sock = _socketSessions.firstOrNull();
|
||||||
if(sock != null){
|
if(sock != null){
|
||||||
sock.send(opcode, ByteBuffer.wrap(data));
|
sock.send(opcode, subOpcode, ByteBuffer.wrap(data));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw IllegalStateException("Session has no active sockets");
|
throw IllegalStateException("Session has no active sockets");
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.sync.internal
|
||||||
|
|
||||||
import com.futo.platformplayer.LittleEndianDataInputStream
|
import com.futo.platformplayer.LittleEndianDataInputStream
|
||||||
import com.futo.platformplayer.LittleEndianDataOutputStream
|
import com.futo.platformplayer.LittleEndianDataOutputStream
|
||||||
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.noise.protocol.CipherStatePair
|
import com.futo.platformplayer.noise.protocol.CipherStatePair
|
||||||
import com.futo.platformplayer.noise.protocol.DHState
|
import com.futo.platformplayer.noise.protocol.DHState
|
||||||
|
@ -18,7 +19,8 @@ class SyncSocketSession {
|
||||||
NOTIFY_UNAUTHORIZED(3u),
|
NOTIFY_UNAUTHORIZED(3u),
|
||||||
STREAM_START(4u),
|
STREAM_START(4u),
|
||||||
STREAM_DATA(5u),
|
STREAM_DATA(5u),
|
||||||
STREAM_END(6u)
|
STREAM_END(6u),
|
||||||
|
DATA(7u)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _inputStream: LittleEndianDataInputStream
|
private val _inputStream: LittleEndianDataInputStream
|
||||||
|
@ -41,12 +43,12 @@ class SyncSocketSession {
|
||||||
private val _localKeyPair: DHState
|
private val _localKeyPair: DHState
|
||||||
private var _localPublicKey: String
|
private var _localPublicKey: String
|
||||||
val localPublicKey: String get() = _localPublicKey
|
val localPublicKey: String get() = _localPublicKey
|
||||||
private val _onData: (session: SyncSocketSession, opcode: UByte, data: ByteBuffer) -> Unit
|
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
|
||||||
var authorizable: IAuthorizable? = null
|
var authorizable: IAuthorizable? = null
|
||||||
|
|
||||||
val remoteAddress: String
|
val remoteAddress: String
|
||||||
|
|
||||||
constructor(remoteAddress: String, localKeyPair: DHState, inputStream: LittleEndianDataInputStream, outputStream: LittleEndianDataOutputStream, onClose: (session: SyncSocketSession) -> Unit, onHandshakeComplete: (session: SyncSocketSession) -> Unit, onData: (session: SyncSocketSession, opcode: UByte, data: ByteBuffer) -> Unit) {
|
constructor(remoteAddress: String, localKeyPair: DHState, inputStream: LittleEndianDataInputStream, outputStream: LittleEndianDataOutputStream, onClose: (session: SyncSocketSession) -> Unit, onHandshakeComplete: (session: SyncSocketSession) -> Unit, onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit) {
|
||||||
_inputStream = inputStream
|
_inputStream = inputStream
|
||||||
_outputStream = outputStream
|
_outputStream = outputStream
|
||||||
_onClose = onClose
|
_onClose = onClose
|
||||||
|
@ -159,10 +161,11 @@ class SyncSocketSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performVersionCheck() {
|
private fun performVersionCheck() {
|
||||||
_outputStream.writeInt(1)
|
val CURRENT_VERSION = 2
|
||||||
|
_outputStream.writeInt(CURRENT_VERSION)
|
||||||
val version = _inputStream.readInt()
|
val version = _inputStream.readInt()
|
||||||
Logger.i(TAG, "performVersionCheck (version = $version)")
|
Logger.i(TAG, "performVersionCheck (version = $version)")
|
||||||
if (version != 1)
|
if (version != CURRENT_VERSION)
|
||||||
throw Exception("Invalid version")
|
throw Exception("Invalid version")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,8 +208,9 @@ class SyncSocketSession {
|
||||||
throw Exception("Handshake finished without completing")
|
throw Exception("Handshake finished without completing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun send(opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
||||||
|
ensureNotMainThread()
|
||||||
|
|
||||||
fun send(opcode: UByte, data: ByteBuffer) {
|
|
||||||
if (data.remaining() + HEADER_SIZE > MAXIMUM_PACKET_SIZE) {
|
if (data.remaining() + HEADER_SIZE > MAXIMUM_PACKET_SIZE) {
|
||||||
val segmentSize = MAXIMUM_PACKET_SIZE - HEADER_SIZE
|
val segmentSize = MAXIMUM_PACKET_SIZE - HEADER_SIZE
|
||||||
val segmentData = ByteArray(segmentSize)
|
val segmentData = ByteArray(segmentSize)
|
||||||
|
@ -223,8 +227,8 @@ class SyncSocketSession {
|
||||||
|
|
||||||
if (sendOffset == 0) {
|
if (sendOffset == 0) {
|
||||||
segmentOpcode = Opcode.STREAM_START.value
|
segmentOpcode = Opcode.STREAM_START.value
|
||||||
bytesToSend = segmentSize - 4 - 4 - 1
|
bytesToSend = segmentSize - 4 - 4 - 1 - 1
|
||||||
segmentPacketSize = bytesToSend + 4 + 4 + 1
|
segmentPacketSize = bytesToSend + 4 + 4 + 1 + 1
|
||||||
} else {
|
} else {
|
||||||
bytesToSend = minOf(segmentSize - 4 - 4, bytesRemaining)
|
bytesToSend = minOf(segmentSize - 4 - 4, bytesRemaining)
|
||||||
segmentOpcode = if (bytesToSend >= bytesRemaining) Opcode.STREAM_END.value else Opcode.STREAM_DATA.value
|
segmentOpcode = if (bytesToSend >= bytesRemaining) Opcode.STREAM_END.value else Opcode.STREAM_DATA.value
|
||||||
|
@ -236,18 +240,20 @@ class SyncSocketSession {
|
||||||
putInt(if (segmentOpcode == Opcode.STREAM_START.value) data.remaining() else sendOffset)
|
putInt(if (segmentOpcode == Opcode.STREAM_START.value) data.remaining() else sendOffset)
|
||||||
if (segmentOpcode == Opcode.STREAM_START.value) {
|
if (segmentOpcode == Opcode.STREAM_START.value) {
|
||||||
put(opcode.toByte())
|
put(opcode.toByte())
|
||||||
|
put(subOpcode.toByte())
|
||||||
}
|
}
|
||||||
put(data.array(), data.position() + sendOffset, bytesToSend)
|
put(data.array(), data.position() + sendOffset, bytesToSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(segmentOpcode, ByteBuffer.wrap(segmentData, 0, segmentPacketSize))
|
send(segmentOpcode, 0u, ByteBuffer.wrap(segmentData, 0, segmentPacketSize))
|
||||||
sendOffset += bytesToSend
|
sendOffset += bytesToSend
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
synchronized(_sendLockObject) {
|
synchronized(_sendLockObject) {
|
||||||
ByteBuffer.wrap(_sendBuffer).order(ByteOrder.LITTLE_ENDIAN).apply {
|
ByteBuffer.wrap(_sendBuffer).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||||
putInt(data.remaining() + 1)
|
putInt(data.remaining() + 2)
|
||||||
put(opcode.toByte())
|
put(opcode.toByte())
|
||||||
|
put(subOpcode.toByte())
|
||||||
put(data.array(), data.position(), data.remaining())
|
put(data.array(), data.position(), data.remaining())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,12 +266,15 @@ class SyncSocketSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun send(opcode: UByte) {
|
fun send(opcode: UByte, subOpcode: UByte = 0u) {
|
||||||
synchronized(_sendLockObject) {
|
ensureNotMainThread()
|
||||||
ByteBuffer.wrap(_sendBuffer, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(1)
|
|
||||||
_sendBuffer.asUByteArray()[4] = opcode
|
|
||||||
|
|
||||||
//Logger.i(TAG, "Encrypting message (size = ${HEADER_SIZE})")
|
synchronized(_sendLockObject) {
|
||||||
|
ByteBuffer.wrap(_sendBuffer, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(2)
|
||||||
|
_sendBuffer.asUByteArray()[4] = opcode
|
||||||
|
_sendBuffer.asUByteArray()[5] = subOpcode
|
||||||
|
|
||||||
|
//Logger.i(TAG, "Encrypting message (opcode = ${opcode}, subOpcode = ${subOpcode}, size = ${HEADER_SIZE})")
|
||||||
|
|
||||||
val len = _cipherStatePair!!.sender.encryptWithAd(null, _sendBuffer, 0, _sendBufferEncrypted, 0, HEADER_SIZE)
|
val len = _cipherStatePair!!.sender.encryptWithAd(null, _sendBuffer, 0, _sendBufferEncrypted, 0, HEADER_SIZE)
|
||||||
//Logger.i(TAG, "Sending encrypted message (size = ${len})")
|
//Logger.i(TAG, "Sending encrypted message (size = ${len})")
|
||||||
|
@ -277,19 +286,19 @@ class SyncSocketSession {
|
||||||
|
|
||||||
private fun handleData(data: ByteArray, length: Int) {
|
private fun handleData(data: ByteArray, length: Int) {
|
||||||
if (length < HEADER_SIZE)
|
if (length < HEADER_SIZE)
|
||||||
throw Exception("Packet must be at least 5 bytes (header size)")
|
throw Exception("Packet must be at least 6 bytes (header size)")
|
||||||
|
|
||||||
val size = ByteBuffer.wrap(data, 0, 4).order(ByteOrder.LITTLE_ENDIAN).int
|
val size = ByteBuffer.wrap(data, 0, 4).order(ByteOrder.LITTLE_ENDIAN).int
|
||||||
if (size != length - 4)
|
if (size != length - 4)
|
||||||
throw Exception("Incomplete packet received")
|
throw Exception("Incomplete packet received")
|
||||||
|
|
||||||
val opcode = data.asUByteArray()[4]
|
val opcode = data.asUByteArray()[4]
|
||||||
val packetData = ByteBuffer.wrap(data, HEADER_SIZE, size - 1)
|
val subOpcode = data.asUByteArray()[5]
|
||||||
|
val packetData = ByteBuffer.wrap(data, HEADER_SIZE, size - 2)
|
||||||
handlePacket(opcode, packetData.order(ByteOrder.LITTLE_ENDIAN))
|
handlePacket(opcode, subOpcode, packetData.order(ByteOrder.LITTLE_ENDIAN))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePacket(opcode: UByte, data: ByteBuffer) {
|
private fun handlePacket(opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
||||||
when (opcode) {
|
when (opcode) {
|
||||||
Opcode.PING.value -> {
|
Opcode.PING.value -> {
|
||||||
send(Opcode.PONG.value)
|
send(Opcode.PONG.value)
|
||||||
|
@ -302,7 +311,7 @@ class SyncSocketSession {
|
||||||
}
|
}
|
||||||
Opcode.NOTIFY_AUTHORIZED.value,
|
Opcode.NOTIFY_AUTHORIZED.value,
|
||||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||||
_onData.invoke(this, opcode, data)
|
_onData.invoke(this, opcode, subOpcode, data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,8 +325,9 @@ class SyncSocketSession {
|
||||||
val id = data.int
|
val id = data.int
|
||||||
val expectedSize = data.int
|
val expectedSize = data.int
|
||||||
val op = data.get().toUByte()
|
val op = data.get().toUByte()
|
||||||
|
val subOp = data.get().toUByte()
|
||||||
|
|
||||||
val syncStream = SyncStream(expectedSize, op)
|
val syncStream = SyncStream(expectedSize, op, subOp)
|
||||||
if (data.remaining() > 0) {
|
if (data.remaining() > 0) {
|
||||||
syncStream.add(data.array(), data.position(), data.remaining())
|
syncStream.add(data.array(), data.position(), data.remaining())
|
||||||
}
|
}
|
||||||
|
@ -362,10 +372,13 @@ class SyncSocketSession {
|
||||||
throw Exception("After sync stream end, the stream must be complete")
|
throw Exception("After sync stream end, the stream must be complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePacket(syncStream.opcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) })
|
handlePacket(syncStream.opcode, syncStream.subOpcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) })
|
||||||
|
}
|
||||||
|
Opcode.DATA.value -> {
|
||||||
|
_onData.invoke(this, opcode, subOpcode, data)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
_onData.invoke(this, opcode, data)
|
Logger.w(TAG, "Unknown opcode received (opcode = ${opcode}, subOpcode = ${subOpcode})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,6 +387,6 @@ class SyncSocketSession {
|
||||||
private const val TAG = "SyncSocketSession"
|
private const val TAG = "SyncSocketSession"
|
||||||
const val MAXIMUM_PACKET_SIZE = 65535 - 16
|
const val MAXIMUM_PACKET_SIZE = 65535 - 16
|
||||||
const val MAXIMUM_PACKET_SIZE_ENCRYPTED = MAXIMUM_PACKET_SIZE + 16
|
const val MAXIMUM_PACKET_SIZE_ENCRYPTED = MAXIMUM_PACKET_SIZE + 16
|
||||||
const val HEADER_SIZE = 5
|
const val HEADER_SIZE = 6
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package com.futo.platformplayer.sync.internal
|
package com.futo.platformplayer.sync.internal
|
||||||
|
|
||||||
class SyncStream(expectedSize: Int, val opcode: UByte) {
|
class SyncStream(expectedSize: Int, val opcode: UByte, val subOpcode: UByte) {
|
||||||
companion object {
|
companion object {
|
||||||
const val MAXIMUM_SIZE = 10_000_000
|
const val MAXIMUM_SIZE = 10_000_000
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.futo.platformplayer.sync.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.Dictionary
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SyncPlaylistsPackage(
|
||||||
|
var playlists: List<Playlist>,
|
||||||
|
var playlistRemovals: Map<String, Long>
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.futo.platformplayer.sync.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.Dictionary
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SyncSubscriptionGroupsPackage(
|
||||||
|
var groups: List<SubscriptionGroup>,
|
||||||
|
var groupRemovals: Map<String, Long>
|
||||||
|
)
|
|
@ -6,6 +6,8 @@ import android.widget.FrameLayout
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
@ -53,14 +55,30 @@ class VideoListEditorView : FrameLayout {
|
||||||
};
|
};
|
||||||
|
|
||||||
adapterVideos.onRemove.subscribe { v ->
|
adapterVideos.onRemove.subscribe { v ->
|
||||||
synchronized(_videos) {
|
val executeDelete = {
|
||||||
val index = _videos.indexOf(v);
|
synchronized(_videos) {
|
||||||
if(index >= 0) {
|
val index = _videos.indexOf(v);
|
||||||
_videos.removeAt(index);
|
if(index >= 0) {
|
||||||
onVideoRemoved.emit(v);
|
_videos.removeAt(index);
|
||||||
|
onVideoRemoved.emit(v);
|
||||||
|
}
|
||||||
|
adapterVideos.notifyItemRemoved(index);
|
||||||
}
|
}
|
||||||
adapterVideos.notifyItemRemoved(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.instance.other.playlistDeleteConfirmation) {
|
||||||
|
UIDialogs.showConfirmationDialog(context, "Please confirm to delete", action = {
|
||||||
|
executeDelete()
|
||||||
|
}, cancelAction = {
|
||||||
|
|
||||||
|
}, doNotAskAgainAction = {
|
||||||
|
Settings.instance.other.playlistDeleteConfirmation = false
|
||||||
|
Settings.instance.save()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
executeDelete()
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
adapterVideos.onClick.subscribe(onVideoClicked::emit);
|
adapterVideos.onClick.subscribe(onVideoClicked::emit);
|
||||||
|
|
||||||
|
|
|
@ -327,8 +327,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||||
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } };
|
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) {
|
fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) {
|
||||||
swapSources(videoSource, audioSource,false, play, keepSubtitles);
|
swapSources(videoSource, audioSource,resume, play, keepSubtitles);
|
||||||
}
|
}
|
||||||
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
||||||
var videoSourceUsed = videoSource;
|
var videoSourceUsed = videoSource;
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingRight="20dp"
|
android:paddingRight="20dp"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingRight="20dp"
|
android:paddingRight="20dp"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingRight="20dp"
|
android:paddingRight="20dp"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
android:id="@+id/button_close"
|
android:id="@+id/button_close"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_close"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
android:id="@+id/incognito_button"
|
android:id="@+id/incognito_button"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_incognito_button"
|
||||||
android:src="@drawable/ic_disabled_visible_purple"
|
android:src="@drawable/ic_disabled_visible_purple"
|
||||||
android:background="@drawable/background_button_round_black"
|
android:background="@drawable/background_button_round_black"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingRight="20dp"
|
android:paddingRight="20dp"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
android:id="@+id/button_help"
|
android:id="@+id/button_help"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_help"
|
||||||
app:srcCompat="@drawable/ic_help"
|
app:srcCompat="@drawable/ic_help"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
android:id="@+id/button_help"
|
android:id="@+id/button_help"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_help"
|
||||||
app:srcCompat="@drawable/ic_help"
|
app:srcCompat="@drawable/ic_help"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
android:id="@+id/button_help"
|
android:id="@+id/button_help"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_help"
|
||||||
app:srcCompat="@drawable/ic_help"
|
app:srcCompat="@drawable/ic_help"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
android:id="@+id/button_help"
|
android:id="@+id/button_help"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_help"
|
||||||
app:srcCompat="@drawable/ic_help"
|
app:srcCompat="@drawable/ic_help"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
android:id="@+id/button_help"
|
android:id="@+id/button_help"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_help"
|
||||||
app:srcCompat="@drawable/ic_help"
|
app:srcCompat="@drawable/ic_help"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
@ -28,6 +30,7 @@
|
||||||
android:id="@+id/image_polycentric"
|
android:id="@+id/image_polycentric"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
|
android:contentDescription="@string/cd_image_polycentric"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:shapeAppearanceOverlay="@style/roundedCorners_40dp"
|
app:shapeAppearanceOverlay="@style/roundedCorners_40dp"
|
||||||
app:srcCompat="@drawable/placeholder_profile"
|
app:srcCompat="@drawable/placeholder_profile"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingRight="20dp"
|
android:paddingRight="20dp"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
android:id="@+id/button_cancel"
|
android:id="@+id/button_cancel"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_button_close"
|
||||||
app:srcCompat="@drawable/ic_close_thin"
|
app:srcCompat="@drawable/ic_close_thin"
|
||||||
app:tint="#888888"
|
app:tint="#888888"
|
||||||
android:layout_marginEnd="30dp" />
|
android:layout_marginEnd="30dp" />
|
||||||
|
@ -71,6 +72,16 @@
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
android:hint="@string/backup_password" />
|
android:hint="@string/backup_password" />
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_password2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginLeft="30dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginRight="30dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:hint="@string/repeat_password" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
android:id="@+id/button_scan_qr"
|
android:id="@+id/button_scan_qr"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_scan_qr"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:srcCompat="@drawable/ic_qr"
|
app:srcCompat="@drawable/ic_qr"
|
||||||
app:tint="@color/primary" />
|
app:tint="@color/primary" />
|
||||||
|
@ -109,6 +110,7 @@
|
||||||
android:id="@+id/button_add"
|
android:id="@+id/button_add"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_add"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:srcCompat="@drawable/ic_add"
|
app:srcCompat="@drawable/ic_add"
|
||||||
app:tint="@color/primary"
|
app:tint="@color/primary"
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
android:id="@+id/image_device"
|
android:id="@+id/image_device"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_image_device"
|
||||||
app:srcCompat="@drawable/ic_chromecast"
|
app:srcCompat="@drawable/ic_chromecast"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
@ -197,6 +198,7 @@
|
||||||
android:id="@id/button_previous"
|
android:id="@id/button_previous"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="@string/cd_button_previous"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
|
@ -206,6 +208,7 @@
|
||||||
android:id="@+id/button_play"
|
android:id="@+id/button_play"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="@string/cd_button_play"
|
||||||
android:padding="20dp"
|
android:padding="20dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
|
@ -215,6 +218,7 @@
|
||||||
android:id="@+id/button_pause"
|
android:id="@+id/button_pause"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="@string/cd_button_pause"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
|
@ -224,6 +228,7 @@
|
||||||
android:id="@+id/button_stop"
|
android:id="@+id/button_stop"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="@string/cd_button_stop"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:padding="5dp"
|
android:padding="5dp"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
|
@ -233,6 +238,7 @@
|
||||||
android:id="@id/button_next"
|
android:id="@id/button_next"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="@string/cd_button_next"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
android:id="@+id/update_spinner"
|
android:id="@+id/update_spinner"
|
||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
|
android:contentDescription="@string/cd_update_spinner"
|
||||||
app:srcCompat="@drawable/ic_update_animated" />
|
app:srcCompat="@drawable/ic_update_animated" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
android:id="@+id/app_icon"
|
android:id="@+id/app_icon"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_app_icon"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
android:id="@+id/button_cast"
|
android:id="@+id/button_cast"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_cast_button"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
|
@ -84,6 +86,7 @@
|
||||||
android:id="@+id/button_add"
|
android:id="@+id/button_add"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_add"
|
||||||
android:paddingStart="5dp"
|
android:paddingStart="5dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingTop="7dp"
|
android:paddingTop="7dp"
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
android:background="@drawable/rounded_outline"
|
android:background="@drawable/rounded_outline"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
@ -103,6 +104,7 @@
|
||||||
android:id="@+id/button_sub_settings"
|
android:id="@+id/button_sub_settings"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
|
android:contentDescription="@string/cd_button_settings"
|
||||||
android:layout_marginTop="3dp"
|
android:layout_marginTop="3dp"
|
||||||
android:layout_marginRight="10dp"
|
android:layout_marginRight="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
@ -114,6 +116,7 @@
|
||||||
android:id="@+id/button_subscribe"
|
android:id="@+id/button_subscribe"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_subscribe"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
android:id="@+id/image_channel_thumbnail"
|
android:id="@+id/image_channel_thumbnail"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
app:srcCompat="@drawable/ic_peertube"
|
app:srcCompat="@drawable/ic_peertube"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
android:id="@+id/button_clear_search"
|
android:id="@+id/button_clear_search"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_clear_search"
|
||||||
android:paddingStart="18dp"
|
android:paddingStart="18dp"
|
||||||
android:paddingEnd="18dp"
|
android:paddingEnd="18dp"
|
||||||
android:layout_gravity="right|center_vertical"
|
android:layout_gravity="right|center_vertical"
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="26dp"
|
android:layout_width="26dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_search_icon"
|
||||||
app:srcCompat="@drawable/ic_search_thin"
|
app:srcCompat="@drawable/ic_search_thin"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
android:id="@+id/button_clear_search"
|
android:id="@+id/button_clear_search"
|
||||||
android:layout_width="46dp"
|
android:layout_width="46dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_clear_search"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_close_thin"
|
app:srcCompat="@drawable/ic_close_thin"
|
||||||
app:tint="@color/gray_ac"
|
app:tint="@color/gray_ac"
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
app:srcCompat="@drawable/ic_back_nav" />
|
app:srcCompat="@drawable/ic_back_nav" />
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
app:srcCompat="@drawable/ic_back_nav" />
|
app:srcCompat="@drawable/ic_back_nav" />
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
android:id="@+id/button_cast"
|
android:id="@+id/button_cast"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_cast_button"
|
||||||
android:paddingStart="4dp"
|
android:paddingStart="4dp"
|
||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:paddingTop="9dp"
|
android:paddingTop="9dp"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/app_icon"
|
android:id="@+id/app_icon"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_app_icon"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
android:id="@+id/button_cast"
|
android:id="@+id/button_cast"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_cast_button"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
|
@ -49,6 +51,7 @@
|
||||||
android:id="@+id/button_search"
|
android:id="@+id/button_search"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_search"
|
||||||
android:paddingStart="5dp"
|
android:paddingStart="5dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingTop="11dp"
|
android:paddingTop="11dp"
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
android:id="@+id/image_history"
|
android:id="@+id/image_history"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_icon_history"
|
||||||
app:srcCompat="@drawable/ic_clock_white"
|
app:srcCompat="@drawable/ic_clock_white"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
@ -119,6 +120,7 @@
|
||||||
android:id="@+id/button_create_playlist"
|
android:id="@+id/button_create_playlist"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_button_create_playlist"
|
||||||
app:srcCompat="@drawable/ic_add_white_16dp"
|
app:srcCompat="@drawable/ic_add_white_16dp"
|
||||||
android:paddingEnd="15dp"
|
android:paddingEnd="15dp"
|
||||||
android:paddingStart="15dp"
|
android:paddingStart="15dp"
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
android:id="@+id/button_share"
|
android:id="@+id/button_share"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_share"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
android:id="@+id/button_back"
|
android:id="@+id/button_back"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingRight="16dp"
|
android:paddingRight="16dp"
|
||||||
app:srcCompat="@drawable/ic_back_white_24dp" />
|
app:srcCompat="@drawable/ic_back_white_24dp" />
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
android:id="@+id/edit_search"
|
android:id="@+id/edit_search"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Search"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
|
@ -37,6 +39,7 @@
|
||||||
android:id="@+id/button_clear_search"
|
android:id="@+id/button_clear_search"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_clear_search"
|
||||||
android:paddingStart="18dp"
|
android:paddingStart="18dp"
|
||||||
android:paddingEnd="18dp"
|
android:paddingEnd="18dp"
|
||||||
android:layout_gravity="right|center_vertical"
|
android:layout_gravity="right|center_vertical"
|
||||||
|
@ -48,6 +51,7 @@
|
||||||
android:id="@+id/button_filter"
|
android:id="@+id/button_filter"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_filter"
|
||||||
android:paddingStart="8dp"
|
android:paddingStart="8dp"
|
||||||
android:paddingEnd="8dp"
|
android:paddingEnd="8dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
android:id="@+id/button_delete"
|
android:id="@+id/button_delete"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
android:layout_marginLeft="5dp"
|
android:layout_marginLeft="5dp"
|
||||||
android:layout_marginRight="0dp"
|
android:layout_marginRight="0dp"
|
||||||
android:src="@drawable/ic_trash"
|
android:src="@drawable/ic_trash"
|
||||||
|
@ -56,6 +57,7 @@
|
||||||
android:id="@+id/button_settings"
|
android:id="@+id/button_settings"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_settings"
|
||||||
android:layout_marginLeft="5dp"
|
android:layout_marginLeft="5dp"
|
||||||
android:layout_marginRight="5dp"
|
android:layout_marginRight="5dp"
|
||||||
android:src="@drawable/ic_settings"
|
android:src="@drawable/ic_settings"
|
||||||
|
@ -69,6 +71,7 @@
|
||||||
android:id="@+id/image_group"
|
android:id="@+id/image_group"
|
||||||
android:layout_width="110dp"
|
android:layout_width="110dp"
|
||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
|
android:contentDescription="@string/cd_image_group"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
app:circularflow_defaultRadius="10dp"
|
app:circularflow_defaultRadius="10dp"
|
||||||
android:layout_marginLeft="30dp"
|
android:layout_marginLeft="30dp"
|
||||||
|
@ -90,6 +93,7 @@
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_edit_image"
|
||||||
android:padding="5dp"
|
android:padding="5dp"
|
||||||
android:clickable="false"
|
android:clickable="false"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
@ -121,6 +125,7 @@
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_button_edit"
|
||||||
android:padding="2dp"
|
android:padding="2dp"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginBottom="-5dp"
|
android:layout_marginBottom="-5dp"
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
android:id="@+id/button_share"
|
android:id="@+id/button_share"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_share"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
|
@ -162,6 +163,7 @@
|
||||||
android:id="@+id/button_edit"
|
android:id="@+id/button_edit"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_edit"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
|
@ -177,6 +179,7 @@
|
||||||
android:id="@+id/button_download"
|
android:id="@+id/button_download"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_download"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
|
|
|
@ -36,7 +36,8 @@
|
||||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="27dp"
|
android:layout_width="27dp"
|
||||||
android:layout_height="27dp" />
|
android:layout_height="27dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail" />
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -128,6 +129,7 @@
|
||||||
android:id="@+id/image_like_icon"
|
android:id="@+id/image_like_icon"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/cd_image_like_icon"
|
||||||
app:srcCompat="@drawable/ic_thumb_up" />
|
app:srcCompat="@drawable/ic_thumb_up" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -144,6 +146,7 @@
|
||||||
android:id="@+id/image_dislike_icon"
|
android:id="@+id/image_dislike_icon"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/cd_image_dislike_icon"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
app:srcCompat="@drawable/ic_thumb_down" />
|
app:srcCompat="@drawable/ic_thumb_down" />
|
||||||
|
@ -285,6 +288,7 @@
|
||||||
android:id="@+id/button_share"
|
android:id="@+id/button_share"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
android:contentDescription="@string/cd_button_share"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
|
|
|
@ -103,6 +103,7 @@
|
||||||
android:id="@+id/minimize_play"
|
android:id="@+id/minimize_play"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_minimize_play"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
@ -111,6 +112,7 @@
|
||||||
android:id="@+id/minimize_pause"
|
android:id="@+id/minimize_pause"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_minimize_pause"
|
||||||
android:padding="5dp"
|
android:padding="5dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
|
@ -119,6 +121,7 @@
|
||||||
android:id="@+id/minimize_close"
|
android:id="@+id/minimize_close"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_minimize_close"
|
||||||
android:padding="5dp"
|
android:padding="5dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
|
@ -337,7 +340,8 @@
|
||||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp" />
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail" />
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
android:id="@+id/donation_amount"
|
android:id="@+id/donation_amount"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_donation_amount"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="$100" />
|
tools:text="$100" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -100,6 +100,7 @@
|
||||||
android:id="@+id/image_like_icon"
|
android:id="@+id/image_like_icon"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/cd_image_like_icon"
|
||||||
app:srcCompat="@drawable/ic_thumb_up" />
|
app:srcCompat="@drawable/ic_thumb_up" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -116,6 +117,7 @@
|
||||||
android:id="@+id/image_dislike_icon"
|
android:id="@+id/image_dislike_icon"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/cd_image_dislike_icon"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
app:srcCompat="@drawable/ic_thumb_down" />
|
app:srcCompat="@drawable/ic_thumb_down" />
|
||||||
|
@ -134,6 +136,7 @@
|
||||||
android:id="@+id/button_replies"
|
android:id="@+id/button_replies"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_replies"
|
||||||
app:pillIcon="@drawable/ic_forum"
|
app:pillIcon="@drawable/ic_forum"
|
||||||
app:pillText="55 Replies"
|
app:pillText="55 Replies"
|
||||||
android:layout_marginStart="15dp" />
|
android:layout_marginStart="15dp" />
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
android:id="@+id/button_replies"
|
android:id="@+id/button_replies"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_replies"
|
||||||
app:pillIcon="@drawable/ic_forum"
|
app:pillIcon="@drawable/ic_forum"
|
||||||
app:pillText="55 Replies"
|
app:pillText="55 Replies"
|
||||||
android:layout_marginStart="15dp" />
|
android:layout_marginStart="15dp" />
|
||||||
|
@ -112,6 +113,7 @@
|
||||||
android:id="@+id/pill_text"
|
android:id="@+id/pill_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="13dp"
|
android:textSize="13dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
android:id="@+id/button_subscribe"
|
android:id="@+id/button_subscribe"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/cd_button_subscribe"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_channel_metadata"
|
app:layout_constraintTop_toBottomOf="@id/text_channel_metadata"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
android:id="@+id/platform_indicator"
|
android:id="@+id/platform_indicator"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:layout_marginTop="18dp"
|
android:layout_marginTop="18dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:id="@+id/image_device"
|
android:id="@+id/image_device"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_image_device"
|
||||||
app:srcCompat="@drawable/ic_chromecast"
|
app:srcCompat="@drawable/ic_chromecast"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
android:id="@+id/image_loader"
|
android:id="@+id/image_loader"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_image_loader"
|
||||||
app:srcCompat="@drawable/ic_loader_animated"
|
app:srcCompat="@drawable/ic_loader_animated"
|
||||||
android:layout_marginEnd="8dp"/>
|
android:layout_marginEnd="8dp"/>
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
android:id="@+id/donation_author_image"
|
android:id="@+id/donation_author_image"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_donation_author_image"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|
|
@ -173,6 +173,7 @@
|
||||||
android:id="@+id/image_trash"
|
android:id="@+id/image_trash"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
app:srcCompat="@drawable/ic_trash_18dp"
|
app:srcCompat="@drawable/ic_trash_18dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
|
|
@ -45,7 +45,8 @@
|
||||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
android:id="@+id/platform"
|
android:id="@+id/platform"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp" />
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -194,6 +194,7 @@
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
|
@ -272,6 +273,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
tools:src="@drawable/ic_peertube"/>
|
tools:src="@drawable/ic_peertube"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -306,6 +306,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_margin="4dp" />
|
android:layout_margin="4dp" />
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
android:id="@+id/image_drag_drop"
|
android:id="@+id/image_drag_drop"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_drag_drop"
|
||||||
app:srcCompat="@drawable/ic_dragdrop_white"
|
app:srcCompat="@drawable/ic_dragdrop_white"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
@ -116,6 +117,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
|
android:contentDescription="@string/cd_download_indicator"
|
||||||
app:srcCompat="@drawable/download_for_offline" />
|
app:srcCompat="@drawable/download_for_offline" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -174,6 +176,7 @@
|
||||||
android:id="@+id/image_trash"
|
android:id="@+id/image_trash"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
app:srcCompat="@drawable/ic_trash_18dp"
|
app:srcCompat="@drawable/ic_trash_18dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
|
|
|
@ -108,6 +108,7 @@
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
|
@ -161,6 +162,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:scaleType="centerInside" />
|
android:scaleType="centerInside" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
android:id="@+id/button_trash"
|
android:id="@+id/button_trash"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
app:srcCompat="@drawable/ic_trash"
|
app:srcCompat="@drawable/ic_trash"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
android:id="@+id/platform_indicator"
|
android:id="@+id/platform_indicator"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
||||||
|
@ -157,6 +158,7 @@
|
||||||
android:id="@+id/image_like_icon"
|
android:id="@+id/image_like_icon"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/cd_image_like_icon"
|
||||||
app:srcCompat="@drawable/ic_thumb_up" />
|
app:srcCompat="@drawable/ic_thumb_up" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -173,6 +175,7 @@
|
||||||
android:id="@+id/image_dislike_icon"
|
android:id="@+id/image_dislike_icon"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/cd_image_dislike_icon"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
app:srcCompat="@drawable/ic_thumb_down" />
|
app:srcCompat="@drawable/ic_thumb_down" />
|
||||||
|
@ -202,6 +205,7 @@
|
||||||
android:id="@+id/image_comments"
|
android:id="@+id/image_comments"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@string/Replies"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
app:srcCompat="@drawable/ic_forum" />
|
app:srcCompat="@drawable/ic_forum" />
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
android:id="@+id/platform_indicator"
|
android:id="@+id/platform_indicator"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
tools:src="@drawable/ic_peertube"
|
tools:src="@drawable/ic_peertube"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
android:id="@+id/image_source"
|
android:id="@+id/image_source"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
app:srcCompat="@drawable/ic_peertube"
|
app:srcCompat="@drawable/ic_peertube"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
android:id="@+id/image_source"
|
android:id="@+id/image_source"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
app:srcCompat="@drawable/ic_peertube"
|
app:srcCompat="@drawable/ic_peertube"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
android:id="@+id/image_drag_drop"
|
android:id="@+id/image_drag_drop"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_drag_drop"
|
||||||
app:srcCompat="@drawable/ic_dragdrop_white"
|
app:srcCompat="@drawable/ic_dragdrop_white"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
android:id="@+id/image_source"
|
android:id="@+id/image_source"
|
||||||
android:layout_width="35dp"
|
android:layout_width="35dp"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
app:srcCompat="@drawable/ic_peertube"
|
app:srcCompat="@drawable/ic_peertube"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="46dp"
|
android:layout_width="46dp"
|
||||||
android:layout_height="46dp"
|
android:layout_height="46dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
android:layout_marginStart="20dp"/>
|
android:layout_marginStart="20dp"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -42,7 +43,8 @@
|
||||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
android:id="@+id/platform"
|
android:id="@+id/platform"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp" />
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator" />
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_meta"
|
android:id="@+id/text_meta"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -60,6 +62,7 @@
|
||||||
android:id="@+id/button_settings"
|
android:id="@+id/button_settings"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_settings"
|
||||||
app:srcCompat="@drawable/ic_settings"
|
app:srcCompat="@drawable/ic_settings"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingStart="5dp"
|
android:paddingStart="5dp"
|
||||||
|
@ -70,6 +73,7 @@
|
||||||
android:id="@+id/button_trash"
|
android:id="@+id/button_trash"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
app:srcCompat="@drawable/ic_trash"
|
app:srcCompat="@drawable/ic_trash"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingStart="5dp"
|
android:paddingStart="5dp"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
android:id="@+id/thumb"
|
android:id="@+id/thumb"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_drag_drop"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
android:id="@+id/image"
|
android:id="@+id/image"
|
||||||
android:layout_width="75dp"
|
android:layout_width="75dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_image_group"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toRightOf="@id/thumb"
|
app:layout_constraintLeft_toRightOf="@id/thumb"
|
||||||
|
@ -77,6 +79,7 @@
|
||||||
android:id="@+id/button_trash"
|
android:id="@+id/button_trash"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_delete"
|
||||||
android:layout_marginRight="10dp"
|
android:layout_marginRight="10dp"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
android:id="@+id/image_drag_drop"
|
android:id="@+id/image_drag_drop"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_drag_drop"
|
||||||
app:srcCompat="@drawable/ic_dragdrop_white"
|
app:srcCompat="@drawable/ic_dragdrop_white"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
|
|
@ -134,6 +134,7 @@
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
@ -205,6 +206,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_download"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/download_for_offline" />
|
app:srcCompat="@drawable/download_for_offline" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -213,6 +215,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
tools:src="@drawable/ic_peertube" />
|
tools:src="@drawable/ic_peertube" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -230,6 +233,7 @@
|
||||||
android:id="@+id/button_add_to_watch_later"
|
android:id="@+id/button_add_to_watch_later"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
|
android:contentDescription="@string/cd_button_add_to_watch_later"
|
||||||
android:layout_marginEnd="5dp"
|
android:layout_marginEnd="5dp"
|
||||||
android:background="@drawable/edit_text_background"
|
android:background="@drawable/edit_text_background"
|
||||||
app:srcCompat="@drawable/ic_clock_white" />
|
app:srcCompat="@drawable/ic_clock_white" />
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
android:id="@+id/thumbnail_platform_nested"
|
android:id="@+id/thumbnail_platform_nested"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
|
@ -173,6 +174,7 @@
|
||||||
android:id="@+id/creator_thumbnail"
|
android:id="@+id/creator_thumbnail"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
|
android:contentDescription="@string/cd_creator_thumbnail"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
|
@ -242,6 +244,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_button_download"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/download_for_offline" />
|
app:srcCompat="@drawable/download_for_offline" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -250,6 +253,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
tools:src="@drawable/ic_peertube"/>
|
tools:src="@drawable/ic_peertube"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -267,6 +271,7 @@
|
||||||
android:id="@+id/button_add_to_watch_later"
|
android:id="@+id/button_add_to_watch_later"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
|
android:contentDescription="@string/cd_button_add_to_watch_later"
|
||||||
android:layout_marginEnd="5dp"
|
android:layout_marginEnd="5dp"
|
||||||
android:background="@drawable/edit_text_background"
|
android:background="@drawable/edit_text_background"
|
||||||
app:srcCompat="@drawable/ic_clock_white" />
|
app:srcCompat="@drawable/ic_clock_white" />
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:contentDescription="@string/cd_download_indicator"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
app:srcCompat="@drawable/download_for_offline" />
|
app:srcCompat="@drawable/download_for_offline" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -112,6 +113,7 @@
|
||||||
android:id="@+id/thumbnail_platform"
|
android:id="@+id/thumbnail_platform"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_platform_indicator"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
|
@ -188,6 +190,7 @@
|
||||||
android:id="@+id/button_add_to_watch_later"
|
android:id="@+id/button_add_to_watch_later"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="27dp"
|
android:layout_height="27dp"
|
||||||
|
android:contentDescription="@string/cd_button_add_to_watch_later"
|
||||||
android:src="@drawable/ic_clock_white"
|
android:src="@drawable/ic_clock_white"
|
||||||
android:paddingTop="7dp"
|
android:paddingTop="7dp"
|
||||||
android:paddingBottom="6dp"
|
android:paddingBottom="6dp"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue