diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
index 23cc959e..8af2e2f5 100644
--- a/CONTRIBUTION.md
+++ b/CONTRIBUTION.md
@@ -49,9 +49,23 @@ We encourage developers to write their own plugins. Please refer to the "Getting
## 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).
---
diff --git a/README.md b/README.md
index 263689a2..b05b5b82 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
-  |
-  |
+  |
+  |
Video |
@@ -24,12 +24,10 @@ The FUTO media app is a player that exposes multiple video websites as sources i
-  |
-  |
+  |
- Sources (all enabled) |
- Sources (one disabled) |
+ Sources |
@@ -38,7 +36,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
 |
-  |
+  |
Install a new source |
@@ -54,8 +52,8 @@ When a user enters a search term into the search bar, the query is posted to th
-  |
-  |
+  |
+  |
Search (list) |
@@ -71,7 +69,7 @@ Creators are able to configure their profile using NeoPass.
-  |
+  |
Channel |
@@ -112,7 +110,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
-  |
+  |
Settings |
@@ -125,8 +123,8 @@ Playlists allow you to make a collection of videos that you can create and custo
-  |
-  |
+  |
+  |
Playlists |
@@ -142,7 +140,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
-  |
+  |
Downloads |
@@ -157,7 +155,7 @@ For more information about casting please click [here](./docs/casting.md).
-  |
+  |
Casting |
@@ -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.
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.
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.
-
## Documentation
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fcf277e6..9ef606c7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -226,10 +226,6 @@
android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
-
+ UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
+ StatePayment.instance.clearLicenses();
+ 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)
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;
}
@@ -919,7 +928,7 @@ class Settings : FragmentedStorageFileJson() {
var enabled: Boolean = true;
@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)
var connectDiscovered: Boolean = true;
diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index e9421cda..519c9c19 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -350,6 +350,13 @@ class UIDialogs {
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) {
val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog);
diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
index c5b15eaf..be7a5e78 100644
--- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
+++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
@@ -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.IPlatformVideoDetails
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.JSDashManifestRawSource
import com.futo.platformplayer.downloads.VideoLocal
@@ -879,6 +880,12 @@ class UISlideOverlays {
val items = arrayListOf();
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) {
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
@@ -899,17 +906,18 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
- SlideUpMenuItem(
- container.context,
- R.drawable.ic_download,
- container.context.getString(R.string.download),
- container.context.getString(R.string.download_the_video),
- tag = "download",
- call = {
- showDownloadVideoOverlay(video, container, true);
- },
- invokeParent = false
- ),
+ if(!isLimited)
+ SlideUpMenuItem(
+ container.context,
+ R.drawable.ic_download,
+ container.context.getString(R.string.download),
+ container.context.getString(R.string.download_the_video),
+ tag = "download",
+ call = {
+ showDownloadVideoOverlay(video, container, true);
+ },
+ invokeParent = false
+ ) else null,
SlideUpMenuItem(
container.context,
R.drawable.ic_share,
@@ -936,7 +944,7 @@ class UISlideOverlays {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
}))
- + actions)
+ + actions).filterNotNull()
));
items.add(
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),
tag = "watch later",
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();
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index e708f8df..76b06525 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -1310,7 +1310,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "TAB";
sourcesIntent.putExtra("TAB", tab);
- sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return sourcesIntent;
}
@@ -1318,7 +1318,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "VIDEO";
sourcesIntent.putExtra("VIDEO", videoUrl);
- sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return sourcesIntent;
}
@@ -1326,14 +1326,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "ACTION";
sourcesIntent.putExtra("ACTION", action);
- sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return sourcesIntent;
}
fun getImportOptionsIntent(context: Context): Intent {
val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "IMPORT_OPTIONS";
- sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return sourcesIntent;
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
index bd3c2ded..3af034aa 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
@@ -50,7 +50,8 @@ class SourcePluginConfig(
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
- var maxDownloadParallelism: Int = 0
+ var maxDownloadParallelism: Int = 0,
+ var reduceFunctionsInLimitedVersion: Boolean = false,
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
index a7672823..8782b742 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
@@ -71,6 +71,8 @@ abstract class JSPager : IPager {
warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow(config, "results", "JSPager");
+ if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
+ throw IllegalStateException("Runtime closed");
val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) }
.toList();
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt
index 93e6a330..18cb086b 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/AutomaticBackupDialog.kt
@@ -22,6 +22,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
private lateinit var _buttonCancel: ImageButton;
private lateinit var _editPassword: EditText;
+ private lateinit var _editPassword2: EditText;
private lateinit var _inputMethodManager: InputMethodManager;
@@ -34,6 +35,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
_buttonStop = findViewById(R.id.button_stop);
_buttonStart = findViewById(R.id.button_start);
_editPassword = findViewById(R.id.edit_password);
+ _editPassword2 = findViewById(R.id.edit_password2);
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
@@ -52,6 +54,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
}
_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();
if(pbytes.size < 4 || pbytes.size > 32) {
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
index 6fca4178..44c2ca2d 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
@@ -10,6 +10,7 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import com.futo.futopay.PaymentConfigurations
import com.futo.futopay.PaymentManager
+import com.futo.futopay.formatMoney
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
@@ -94,9 +95,8 @@ class BuyFragment : MainFragment() {
if(currency != null && prices.containsKey(currency.id)) {
val price = prices[currency.id]!!;
- val priceDecimal = (price.toDouble() / 100);
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);
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt
index f2b4aa8d..9fd4d7d6 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt
@@ -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.Action("Cancel", {}),
UIDialogs.Action("Delete", {
- StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
+ StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id, true);
_didDelete = true;
fragment.close(true);
}, UIDialogs.ActionStyle.DANGEROUS))
@@ -253,7 +253,7 @@ class SubscriptionGroupFragment : MainFragment() {
if(g.urls.isEmpty() && g.image == null) {
//Obtain image
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) {
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
g.image?.setImageView(_imageGroup);
@@ -308,8 +308,10 @@ class SubscriptionGroupFragment : MainFragment() {
if(group != null) {
val urls = group.urls.toList();
- val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
- _enabledCreators.addAll(subs.filter { urls.contains(it.url) });
+ val subs = urls.map {
+ (StateSubscriptions.instance.getSubscription(it) ?: StateSubscriptions.instance.getSubscriptionOther(it))?.channel
+ }.filterNotNull();
+ _enabledCreators.addAll(subs);
}
updateMeta();
filterCreators();
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt
index 71402e60..792aae77 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt
@@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.AddSourceOptionsActivity
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
@@ -57,10 +58,19 @@ class SubscriptionGroupListFragment : MainFragment() {
};
it.onDelete.subscribe { group ->
- val loc = _subs.indexOf(group);
- _subs.remove(group);
- _list?.adapter?.notifyItemRangeRemoved(loc);
- StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
+ context?.let { context ->
+ UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${group.name}]?", null, 0,
+ UIDialogs.Action("Cancel", {}),
+ 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 {
_touchHelper?.startDrag(it);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
index 03d9a626..05f064aa 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
@@ -27,6 +27,7 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
@@ -364,6 +365,7 @@ class SubscriptionsFeedFragment : MainFragment() {
}
override fun reload() {
+ StatePlugins.instance.clearUpdating(); //Fallback in case it doesnt clear, UI should be blocked.
loadResults(true);
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
index 1530537b..233d9dc4 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
@@ -101,7 +101,7 @@ class VideoDetailFragment : MainFragment {
val currentRequestedOrientation = a.requestedOrientation
var currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
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 isFs = isFullscreen
@@ -347,7 +347,7 @@ class VideoDetailFragment : MainFragment {
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)
return true
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index 30d0ab30..5551722d 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -40,6 +40,7 @@ import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
+import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
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.IPlatformVideoDetails
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.models.JSVideoDetails
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.StatePolycentric
import com.futo.platformplayer.states.StateSubscriptions
+import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
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.toHumanBytesSize
import com.futo.platformplayer.toHumanNowDiffString
@@ -637,6 +642,27 @@ class VideoDetailView : ConstraintLayout {
StatePlayer.instance.onVideoChanging.subscribe(this) {
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.onPlayReceived.subscribe(this) { handlePlay() };
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
@@ -711,6 +737,7 @@ class VideoDetailView : ConstraintLayout {
};
onClose.subscribe {
+ checkAndRemoveWatchLater();
_lastVideoSource = null;
_lastAudioSource = null;
_lastSubtitleSource = null;
@@ -819,6 +846,11 @@ class VideoDetailView : ConstraintLayout {
}
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) {
(video ?: _searchVideo)?.let {
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
@@ -838,38 +870,44 @@ class VideoDetailView : ConstraintLayout {
}
_slideUpOverlay?.hide();
} else null,
- RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
- if(!allowBackground) {
- _player.switchToAudioMode();
- allowBackground = true;
- it.text.text = resources.getString(R.string.background_revert);
+ if(!isLimitedVersion)
+ RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
+ if(!allowBackground) {
+ _player.switchToAudioMode();
+ 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 {
- _player.switchToVideoMode();
- allowBackground = false;
- it.text.text = resources.getString(R.string.background);
+ else null,
+ if(!isLimitedVersion)
+ RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
+ video?.let {
+ _slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
+ };
}
- _slideUpOverlay?.hide();
- },
- RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
- video?.let {
- _slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
- };
- },
- RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
- video?.let {
- Logger.i(TAG, "Share preventPictureInPicture = true");
- preventPictureInPicture = true;
- shareVideo();
- };
- _slideUpOverlay?.hide();
- },
- RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
- this.startPictureInPicture();
- fragment.forcePictureInPicture();
- //PiPActivity.startPiP(context);
- _slideUpOverlay?.hide();
- },
+ else null,
+ RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
+ video?.let {
+ Logger.i(TAG, "Share preventPictureInPicture = true");
+ preventPictureInPicture = true;
+ shareVideo();
+ };
+ _slideUpOverlay?.hide();
+ },
+ if(!isLimitedVersion)
+ RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
+ this.startPictureInPicture();
+ fragment.forcePictureInPicture();
+ //PiPActivity.startPiP(context);
+ _slideUpOverlay?.hide();
+ }
+ else null,
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
video?.let {
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
@@ -878,6 +916,22 @@ class VideoDetailView : ConstraintLayout {
};
_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") {
reloadVideo();
_slideUpOverlay?.hide();
@@ -1025,6 +1079,8 @@ class VideoDetailView : ConstraintLayout {
StateApp.instance.preventPictureInPicture.remove(this);
StatePlayer.instance.onQueueChanged.remove(this);
StatePlayer.instance.onVideoChanging.remove(this);
+ StateSync.instance.deviceUpdatedOrAdded.remove(this);
+ StateSync.instance.deviceRemoved.remove(this);
MediaControlReceiver.onLowerVolumeReceived.remove(this);
MediaControlReceiver.onPlayReceived.remove(this);
MediaControlReceiver.onPauseReceived.remove(this);
@@ -1642,7 +1698,7 @@ class VideoDetailView : ConstraintLayout {
});
else
_player.setArtwork(null);
- _player.setSource(videoSource, audioSource, _playWhenReady, false);
+ _player.setSource(videoSource, audioSource, _playWhenReady, false, resume = resumePositionMs > 0);
if(subtitleSource != null)
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
_player.seekTo(resumePositionMs);
@@ -1792,6 +1848,8 @@ class VideoDetailView : ConstraintLayout {
fun prevVideo(withoutRemoval: Boolean = false) {
Logger.i(TAG, "prevVideo")
+ checkAndRemoveWatchLater();
+
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) {
setVideoOverview(next, true, 0, true);
@@ -1800,6 +1858,8 @@ class VideoDetailView : ConstraintLayout {
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
+ checkAndRemoveWatchLater();
+
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
val autoplayVideo = _autoplayVideo
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
@@ -1808,7 +1868,8 @@ class VideoDetailView : ConstraintLayout {
next = autoplayVideo
}
_autoplayVideo = null
- Logger.i(TAG, "Autoplay video cleared (nextVideo)")
+ Logger.i(TAG, "Autoplay video cleared (nextVideo)");
+
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
if(next != null) {
@@ -1820,6 +1881,20 @@ class VideoDetailView : ConstraintLayout {
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
private fun updateQualityFormatsOverlay(liveStreamVideoFormats : List?, liveStreamAudioFormats : List?) {
val v = video ?: return;
@@ -2860,6 +2935,7 @@ class VideoDetailView : ConstraintLayout {
const val TAG_OVERLAY = "overlay";
const val TAG_LIVECHAT = "livechat";
const val TAG_OPEN = "open";
+ const val TAG_SEND_TO_DEVICE = "send_to_device";
const val TAG_MORE = "MORE";
private val _buttonPinStore = FragmentedStorage.get("videoPinnedButtons");
diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
index 23fa2b73..8b4b5847 100644
--- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
+++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
@@ -191,21 +191,21 @@ class VideoHelper {
}
- fun estimateSourceSize(source: IVideoSource?): Int {
+ fun estimateSourceSize(source: IVideoSource?): Long {
if(source == null) return 0;
if(source is IVideoSource) {
if(source.bitrate ?: 0 <= 0 || source.duration.toInt() == 0)
return 0;
- return (source.duration / 8).toInt() * source.bitrate!!;
+ return (source.duration / 8) * source.bitrate!!;
}
else return 0;
}
- fun estimateSourceSize(source: IAudioSource?): Int {
+ fun estimateSourceSize(source: IAudioSource?): Long {
if(source == null) return 0;
if(source is IAudioSource) {
if(source.bitrate <= 0 || source.duration?.toInt() ?: 0 == 0)
return 0;
- return (source.duration!! / 8).toInt() * source.bitrate;
+ return (source.duration!! / 8) * source.bitrate;
}
else return 0;
}
diff --git a/app/src/main/java/com/futo/platformplayer/mdns/MDNSListener.kt b/app/src/main/java/com/futo/platformplayer/mdns/MDNSListener.kt
index 70268ec0..2b972d87 100644
--- a/app/src/main/java/com/futo/platformplayer/mdns/MDNSListener.kt
+++ b/app/src/main/java/com/futo/platformplayer/mdns/MDNSListener.kt
@@ -46,7 +46,10 @@ class MDNSListener {
}
fun start() {
- if (_started) throw Exception("Already running.")
+ if (_started) {
+ Logger.i(TAG, "Already started.")
+ return
+ }
_started = true
_scope = CoroutineScope(Dispatchers.IO);
diff --git a/app/src/main/java/com/futo/platformplayer/mdns/ServiceDiscoverer.kt b/app/src/main/java/com/futo/platformplayer/mdns/ServiceDiscoverer.kt
index 79d29736..f4a3e5e9 100644
--- a/app/src/main/java/com/futo/platformplayer/mdns/ServiceDiscoverer.kt
+++ b/app/src/main/java/com/futo/platformplayer/mdns/ServiceDiscoverer.kt
@@ -37,7 +37,10 @@ class ServiceDiscoverer(names: Array, private val _onServicesUpdated: (L
}
fun start() {
- if (_started) throw Exception("Already running.")
+ if (_started) {
+ Logger.i(TAG, "Already started.")
+ return
+ }
_started = true
val listener = MDNSListener()
diff --git a/app/src/main/java/com/futo/platformplayer/mdns/ServiceRecordAggregator.kt b/app/src/main/java/com/futo/platformplayer/mdns/ServiceRecordAggregator.kt
index a5337dc5..bb4d1007 100644
--- a/app/src/main/java/com/futo/platformplayer/mdns/ServiceRecordAggregator.kt
+++ b/app/src/main/java/com/futo/platformplayer/mdns/ServiceRecordAggregator.kt
@@ -100,33 +100,34 @@ class ServiceRecordAggregator {
Logger.i(TAG, "$builder")*/
val currentServices: MutableList
+ 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) {
- 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()
this._currentServices.clear()
this._currentServices.addAll(currentServices)
diff --git a/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt
index c46aa3b8..b5f34415 100644
--- a/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt
+++ b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt
@@ -1,5 +1,7 @@
package com.futo.platformplayer.models
+import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
+import java.time.OffsetDateTime
import java.util.UUID
@kotlinx.serialization.Serializable
@@ -10,6 +12,11 @@ open class SubscriptionGroup {
var urls: MutableList = mutableListOf();
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) {
this.name = name;
}
@@ -19,6 +26,8 @@ open class SubscriptionGroup {
this.image = parent.image;
this.urls = parent.urls;
this.priority = parent.priority;
+ this.lastChange = parent.lastChange;
+ this.creationTime = parent.creationTime;
}
class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {
diff --git a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt
index a9c83202..c36a1942 100644
--- a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt
+++ b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt
@@ -12,6 +12,8 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex
+import com.futo.platformplayer.views.behavior.NonScrollingTextView
+import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
import kotlinx.coroutines.runBlocking
class PlatformLinkMovementMethod : LinkMovementMethod {
@@ -23,6 +25,7 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
val action = event.action;
+ Logger.i(TAG, "onTouchEvent (action = $action)")
if (action == MotionEvent.ACTION_UP) {
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX;
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY;
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index 52103822..b05d25c8 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -2,6 +2,8 @@ package com.futo.platformplayer.states
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@@ -20,9 +22,13 @@ import androidx.lifecycle.lifecycleScope
import androidx.work.*
import com.futo.platformplayer.*
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.IWithResultLauncher
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.JSClient
import com.futo.platformplayer.background.BackgroundWorker
@@ -419,8 +425,17 @@ class StateApp {
Logger.onLogSubmitted.subscribe {
scopeOrNull?.launch(Dispatchers.Main) {
try {
- if (it != null) {
- UIDialogs.toast("Uploaded $it", true);
+ if (!it.isNullOrEmpty()) {
+ (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 {
UIDialogs.toast("Failed to upload");
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt
index dcebaf95..7cf3d976 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt
@@ -11,6 +11,7 @@ import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
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.copyTo
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.getNowDiffHours
import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.ImportCache
+import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
@@ -61,9 +64,9 @@ class StateBackup {
StatePlaylists.instance.toMigrateCheck()
).flatten();
- fun getCache(): ImportCache {
+ fun getCache(additionalVideos: List = listOf()): ImportCache {
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 channels = allSubscriptions.map { it.channel };
@@ -240,6 +243,23 @@ class StateBackup {
.associateBy { it.name }
.mapValues { it.value.getAllReconstructionStrings() }
.toMutableMap();
+
+ var historyVideos: List? = 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 pluginSettings = StatePlugins.instance.getPlugins()
.associateBy { it.config.id }
@@ -249,7 +269,7 @@ class StateBackup {
.associateBy { it.config.id }
.mapValues { it.value.config.sourceUrl!! };
- val cache = getCache();
+ val cache = getCache(historyVideos ?: listOf());
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
@@ -333,19 +353,64 @@ class StateBackup {
if(doImportStores) {
for(store in export.stores) {
Logger.i(TAG, "Importing store [${store.key}]");
- val relevantStore = availableStores.find { it.name == store.key };
- if(relevantStore == null) {
- Logger.w(TAG, "Unknown store [${store.key}] import");
- continue;
+ if(store.key == "history") {
+ withContext(Dispatchers.Main) {
+ UIDialogs.showDialog(context, R.drawable.ic_move_up, "Import History", "Would you like to import history?", null, 0,
+ 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) {
- UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
- synchronized(toAwait) {
- toAwait.remove(store.key);
- if(toAwait.isEmpty())
- onConclusion();
- }
- };
+ else if(store.key == "subscription_groups") {
+ withContext(Dispatchers.Main) {
+ 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,
+ UIDialogs.Action("No", {
+ }, UIDialogs.ActionStyle.NONE),
+ UIDialogs.Action("Yes", {
+ for(groupStr in store.value) {
+ try {
+ val group = Json.decodeFromString(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();
+ }
+ };
+ }
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
index d5da7880..cf2c032c 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
@@ -59,7 +59,6 @@ class StateHistory {
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 {
val pos = if(position < 0) 0 else position;
val historyVideo = index.obj;
@@ -90,7 +89,7 @@ class StateHistory {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
Logger.i(TAG, "SyncHistory playback broadcasted (${liveObj.name}: ${position})");
- StateSync.instance.broadcastJson(
+ StateSync.instance.broadcastJsonData(
GJSyncOpcodes.syncHistory,
listOf(historyVideo)
);
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
index d5f53af4..f5a033ab 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
@@ -17,10 +17,18 @@ import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist
+import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage
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.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.json.Json
import java.io.File
@@ -45,6 +53,7 @@ class StatePlaylists {
val playlistStore = FragmentedStorage.storeJson("playlists")
.withRestore(PlaylistBackup())
.load();
+ private val _playlistRemoved = FragmentedStorage.get("playlist_removed");
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
@@ -81,6 +90,18 @@ class StatePlaylists {
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) {
synchronized(_watchlistStore) {
_watchlistStore.delete(video);
@@ -118,6 +139,9 @@ class StatePlaylists {
return playlistStore.findItem { it.id == id };
}
+ fun getPlaylistRemovals(): Map {
+ return _playlistRemoved.all();
+ }
fun didPlay(playlistId: String) {
val playlist = getPlaylist(playlistId);
@@ -148,13 +172,15 @@ class StatePlaylists {
createOrUpdatePlaylist(newPlaylist);
return newPlaylist;
}
- fun createOrUpdatePlaylist(playlist: Playlist) {
+ fun createOrUpdatePlaylist(playlist: Playlist, isUserInteraction: Boolean = true) {
playlist.dateUpdate = OffsetDateTime.now();
playlistStore.saveAsync(playlist, true);
if(playlist.id.isNotEmpty()) {
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
}
+ if(isUserInteraction)
+ broadcastSyncPlaylist(playlist);
}
}
fun addToPlaylist(id: String, video: IPlatformVideo) {
@@ -163,14 +189,41 @@ class StatePlaylists {
playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
playlist.dateUpdate = OffsetDateTime.now();
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);
if(StateDownloads.instance.isPlaylistCached(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 {
@@ -194,6 +247,16 @@ class StatePlaylists {
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
}
+
+ fun getSyncPlaylistsPackageString(): String{
+ return Json.encodeToString(
+ SyncPlaylistsPackage(
+ getPlaylists(),
+ getPlaylistRemovals()
+ )
+ );
+ }
+
companion object {
val TAG = "StatePlaylists";
private var _instance : StatePlaylists? = null;
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
index 71aa6d6f..02154677 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
@@ -19,6 +19,7 @@ import com.futo.platformplayer.stores.PluginIconStorage
import com.futo.platformplayer.stores.PluginScriptsDirectory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
@@ -47,6 +48,8 @@ class StatePlugins {
private var _updatesAvailableMap: HashSet = hashSetOf();
+ private val _isUpdating: HashSet = hashSetOf();
+
fun getPluginIconOrNull(id: String): ImageVariable? {
if(iconsDir.hasIcon(id))
return iconsDir.getIconBinary(id);
@@ -58,6 +61,38 @@ class StatePlugins {
.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> = withContext(Dispatchers.IO) {
var configs = mutableListOf>()
@@ -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) {
scope.launch(Dispatchers.IO) {
- val client = ManagedHttpClient();
- try {
+ whileUpdating(config.id) {
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 plugin = JSClient(context, tempDescriptor, null, script);
- plugin.validate();
-
- withContext(Dispatchers.Main) {
- onProgress.invoke("Downloading Icon", 0.5);
- }
-
- val icon = config.absoluteIconUrl?.let { absIconUrl ->
+ val client = ManagedHttpClient();
+ try {
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) {
- onProgress.invoke("Finished", 1.0)
- onConcluded.invoke(null);
- }
- } catch (ex: Exception) {
- Logger.e(TAG, ex.message ?: "null", ex);
- withContext(Dispatchers.Main) {
- onConcluded.invoke(ex);
+ val tempDescriptor = SourcePluginDescriptor(config);
+ val plugin = JSClient(context, tempDescriptor, null, script);
+ plugin.validate();
+
+ withContext(Dispatchers.Main) {
+ onProgress.invoke("Downloading Icon", 0.5);
+ }
+
+ 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);
+ }
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt
index f77e2fad..5ca521ec 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt
@@ -25,13 +25,20 @@ import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
+import com.futo.platformplayer.states.StateHistory.Companion
import com.futo.platformplayer.stores.FragmentedStorage
+import com.futo.platformplayer.stores.StringDateMapStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
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.serialization.encodeToString
+import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
@@ -51,6 +58,9 @@ class StateSubscriptionGroups {
.withUnique { it.id }
.load();
+
+ private val _groupsRemoved = FragmentedStorage.get("group_removed");
+
val onGroupsChanged = Event0();
fun getSubscriptionGroup(id: String): SubscriptionGroup? {
@@ -59,19 +69,66 @@ class StateSubscriptionGroups {
fun getSubscriptionGroups(): List {
return _subGroups.getItems();
}
- fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) {
+ fun getSubscriptionGroupsRemovals(): Map {
+ return _groupsRemoved.all();
+ }
+ fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false, preventSync: Boolean = false) {
+ subGroup.lastChange = OffsetDateTime.now();
_subGroups.save(subGroup);
if(!preventNotify)
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);
if(group != null) {
_subGroups.delete(group);
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 {
const val TAG = "StateSubscriptionGroups";
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
index 8e69d487..52fb9f2e 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
@@ -202,13 +202,13 @@ class StateSubscriptions {
return _subscriptionOthers.findItem { it.isChannel(url)};
}
}
- fun getSubscriptionOtherOrCreate(url: String) : Subscription {
+ fun getSubscriptionOtherOrCreate(url: String, name: String? = null, thumbnail: String? = null) : Subscription {
synchronized(_subscriptionOthers) {
val sub = getSubscriptionOther(url);
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;
- _subscriptions.save(newSub);
+ _subscriptionOthers.save(newSub);
return newSub;
}
else return sub;
@@ -250,7 +250,7 @@ class StateSubscriptions {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
- StateSync.instance.broadcast(
+ StateSync.instance.broadcastData(
GJSyncOpcodes.syncSubscriptions, Json.encodeToString(
SyncSubscriptionsPackage(
listOf(subObj),
@@ -293,8 +293,29 @@ class StateSubscriptions {
if(sub != null) {
_subscriptions.delete(sub);
onSubscriptionsChanged.emit(getSubscriptions(), false);
- if(isUserAction)
- _subscriptionsRemoved.setAndSave(sub.channel.url, OffsetDateTime.now());
+ if(isUserAction) {
+ 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;
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt
index 7c05d20d..4de1b41c 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt
@@ -66,6 +66,10 @@ class StateSync {
val deviceUpdatedOrAdded: Event2 = Event2()
fun start() {
+ if (_started) {
+ Logger.i(TAG, "Already started.")
+ return
+ }
_started = true
if (Settings.instance.synchronization.broadcast || Settings.instance.synchronization.connectDiscovered) {
@@ -366,26 +370,29 @@ class StateSync {
Logger.i(TAG, "Connection authorized for ${remotePublicKey} because initiator")
}
},
- onData = { s, opcode, data ->
- session?.handlePacket(s, opcode, data)
+ onData = { s, opcode, subOpcode, data ->
+ session?.handlePacket(s, opcode, subOpcode, data)
})
}
- inline fun broadcastJson(opcode: UByte, data: T) {
- broadcast(opcode, Json.encodeToString(data));
+ inline fun broadcastJsonData(subOpcode: UByte, data: T) {
+ broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, Json.encodeToString(data));
}
- fun broadcast(opcode: UByte, data: String) {
- broadcast(opcode, data.toByteArray(Charsets.UTF_8));
+ fun broadcastData(subOpcode: UByte, data: String) {
+ 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()) {
try {
if (session.isAuthorized && session.connected) {
- session.send(opcode, data);
+ session.send(opcode, subOpcode, data);
}
}
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 export = StateBackup.export();
//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");
}
diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
index 978500e0..eff83030 100644
--- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
+++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
@@ -22,6 +22,7 @@ import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.states.StateSubscriptions
import kotlinx.coroutines.CoroutineScope
import java.time.OffsetDateTime
@@ -138,6 +139,18 @@ abstract class SubscriptionsTaskFetchAlgorithm(
for(task in tasks) {
val forkTask = threadPool.submit {
+ 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) {
try {
diff --git a/app/src/main/java/com/futo/platformplayer/sync/GJSyncOpcodes.kt b/app/src/main/java/com/futo/platformplayer/sync/GJSyncOpcodes.kt
index a6cef5d4..fdd9eebf 100644
--- a/app/src/main/java/com/futo/platformplayer/sync/GJSyncOpcodes.kt
+++ b/app/src/main/java/com/futo/platformplayer/sync/GJSyncOpcodes.kt
@@ -11,5 +11,7 @@ class GJSyncOpcodes {
val syncSubscriptions: UByte = 202.toUByte();
val syncHistory: UByte = 203.toUByte();
+ val syncSubscriptionGroups: UByte = 204.toUByte();
+ val syncPlaylists: UByte = 205.toUByte();
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt
index 9f6b77f5..cca42a54 100644
--- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt
+++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt
@@ -6,15 +6,20 @@ import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateHistory
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.StateSync
import com.futo.platformplayer.sync.SyncSessionData
import com.futo.platformplayer.sync.internal.SyncSocketSession.Opcode
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -22,7 +27,9 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
+import java.time.Instant
import java.time.OffsetDateTime
+import java.time.ZoneOffset
interface IAuthorizable {
val isAuthorized: Boolean
@@ -38,6 +45,7 @@ class SyncSession : IAuthorizable {
private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit
val remotePublicKey: String
override val isAuthorized get() = _authorized && _remoteAuthorized
+ private var _wasAuthorized = false
var connected: Boolean = false
private set(v) {
@@ -87,8 +95,10 @@ class SyncSession : IAuthorizable {
}
private fun checkAuthorized() {
- if (isAuthorized)
+ if (!_wasAuthorized && isAuthorized) {
+ _wasAuthorized = true
_onAuthorized.invoke(this)
+ }
}
fun removeSocketSession(socketSession: SyncSocketSession) {
@@ -110,29 +120,34 @@ class SyncSession : IAuthorizable {
_onClose.invoke(this)
}
- fun handlePacket(socketSession: SyncSocketSession, opcode: 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
+ fun handlePacket(socketSession: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
try {
+ Logger.i(TAG, "Handle packet (opcode: ${opcode}, subOpcode: ${subOpcode}, 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
+ }
+
+ 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 -> {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
val context = StateApp.instance.contextOrNull;
@@ -157,11 +172,13 @@ class SyncSession : IAuthorizable {
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);
if(recentHistory.size > 0)
- sendJson(GJSyncOpcodes.syncHistory, recentHistory);
+ sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
}
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(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(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 -> {
val dataBody = ByteArray(data.remaining());
data.get(dataBody);
@@ -242,8 +320,7 @@ class SyncSession : IAuthorizable {
if(!StateSubscriptions.instance.isSubscribed(sub.channel)) {
val removalTime = StateSubscriptions.instance.getSubscriptionRemovalTime(sub.channel.url);
if(sub.creationTime > removalTime) {
- val newSub =
- StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
+ val newSub = StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
added.add(newSub);
}
}
@@ -269,16 +346,19 @@ class SyncSession : IAuthorizable {
}
- inline fun sendJson(opcode: UByte, data: T) {
- send(opcode, Json.encodeToString(data));
+ inline fun sendJsonData(subOpcode: UByte, data: T) {
+ send(Opcode.DATA.value, subOpcode, Json.encodeToString(data));
}
- fun send(opcode: UByte, data: String) {
- send(opcode, data.toByteArray(Charsets.UTF_8));
+ fun sendData(subOpcode: UByte, data: String) {
+ 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();
if(sock != null){
- sock.send(opcode, ByteBuffer.wrap(data));
+ sock.send(opcode, subOpcode, ByteBuffer.wrap(data));
}
else
throw IllegalStateException("Session has no active sockets");
diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt
index b7b5ab79..8b5f305a 100644
--- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt
+++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.sync.internal
import com.futo.platformplayer.LittleEndianDataInputStream
import com.futo.platformplayer.LittleEndianDataOutputStream
+import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.noise.protocol.CipherStatePair
import com.futo.platformplayer.noise.protocol.DHState
@@ -18,7 +19,8 @@ class SyncSocketSession {
NOTIFY_UNAUTHORIZED(3u),
STREAM_START(4u),
STREAM_DATA(5u),
- STREAM_END(6u)
+ STREAM_END(6u),
+ DATA(7u)
}
private val _inputStream: LittleEndianDataInputStream
@@ -41,12 +43,12 @@ class SyncSocketSession {
private val _localKeyPair: DHState
private var _localPublicKey: String
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
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
_outputStream = outputStream
_onClose = onClose
@@ -159,10 +161,11 @@ class SyncSocketSession {
}
private fun performVersionCheck() {
- _outputStream.writeInt(1)
+ val CURRENT_VERSION = 2
+ _outputStream.writeInt(CURRENT_VERSION)
val version = _inputStream.readInt()
Logger.i(TAG, "performVersionCheck (version = $version)")
- if (version != 1)
+ if (version != CURRENT_VERSION)
throw Exception("Invalid version")
}
@@ -205,8 +208,9 @@ class SyncSocketSession {
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) {
val segmentSize = MAXIMUM_PACKET_SIZE - HEADER_SIZE
val segmentData = ByteArray(segmentSize)
@@ -223,8 +227,8 @@ class SyncSocketSession {
if (sendOffset == 0) {
segmentOpcode = Opcode.STREAM_START.value
- bytesToSend = segmentSize - 4 - 4 - 1
- segmentPacketSize = bytesToSend + 4 + 4 + 1
+ bytesToSend = segmentSize - 4 - 4 - 1 - 1
+ segmentPacketSize = bytesToSend + 4 + 4 + 1 + 1
} else {
bytesToSend = minOf(segmentSize - 4 - 4, bytesRemaining)
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)
if (segmentOpcode == Opcode.STREAM_START.value) {
put(opcode.toByte())
+ put(subOpcode.toByte())
}
put(data.array(), data.position() + sendOffset, bytesToSend)
}
- send(segmentOpcode, ByteBuffer.wrap(segmentData, 0, segmentPacketSize))
+ send(segmentOpcode, 0u, ByteBuffer.wrap(segmentData, 0, segmentPacketSize))
sendOffset += bytesToSend
}
} else {
synchronized(_sendLockObject) {
ByteBuffer.wrap(_sendBuffer).order(ByteOrder.LITTLE_ENDIAN).apply {
- putInt(data.remaining() + 1)
+ putInt(data.remaining() + 2)
put(opcode.toByte())
+ put(subOpcode.toByte())
put(data.array(), data.position(), data.remaining())
}
@@ -260,12 +266,15 @@ class SyncSocketSession {
}
}
- fun send(opcode: UByte) {
- synchronized(_sendLockObject) {
- ByteBuffer.wrap(_sendBuffer, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(1)
- _sendBuffer.asUByteArray()[4] = opcode
+ fun send(opcode: UByte, subOpcode: UByte = 0u) {
+ ensureNotMainThread()
- //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)
//Logger.i(TAG, "Sending encrypted message (size = ${len})")
@@ -277,19 +286,19 @@ class SyncSocketSession {
private fun handleData(data: ByteArray, length: Int) {
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
if (size != length - 4)
throw Exception("Incomplete packet received")
val opcode = data.asUByteArray()[4]
- val packetData = ByteBuffer.wrap(data, HEADER_SIZE, size - 1)
-
- handlePacket(opcode, packetData.order(ByteOrder.LITTLE_ENDIAN))
+ val subOpcode = data.asUByteArray()[5]
+ val packetData = ByteBuffer.wrap(data, HEADER_SIZE, size - 2)
+ 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) {
Opcode.PING.value -> {
send(Opcode.PONG.value)
@@ -302,7 +311,7 @@ class SyncSocketSession {
}
Opcode.NOTIFY_AUTHORIZED.value,
Opcode.NOTIFY_UNAUTHORIZED.value -> {
- _onData.invoke(this, opcode, data)
+ _onData.invoke(this, opcode, subOpcode, data)
return
}
}
@@ -316,8 +325,9 @@ class SyncSocketSession {
val id = data.int
val expectedSize = data.int
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) {
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")
}
- 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 -> {
- _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"
const val MAXIMUM_PACKET_SIZE = 65535 - 16
const val MAXIMUM_PACKET_SIZE_ENCRYPTED = MAXIMUM_PACKET_SIZE + 16
- const val HEADER_SIZE = 5
+ const val HEADER_SIZE = 6
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt
index 5a60e295..d558feef 100644
--- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt
+++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt
@@ -1,6 +1,6 @@
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 {
const val MAXIMUM_SIZE = 10_000_000
}
diff --git a/app/src/main/java/com/futo/platformplayer/sync/models/SyncPlaylistsPackage.kt b/app/src/main/java/com/futo/platformplayer/sync/models/SyncPlaylistsPackage.kt
new file mode 100644
index 00000000..3d40057f
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/sync/models/SyncPlaylistsPackage.kt
@@ -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,
+ var playlistRemovals: Map
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/sync/models/SyncSubscriptionGroupsPackage.kt b/app/src/main/java/com/futo/platformplayer/sync/models/SyncSubscriptionGroupsPackage.kt
new file mode 100644
index 00000000..663f6a7b
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/sync/models/SyncSubscriptionGroupsPackage.kt
@@ -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,
+ var groupRemovals: Map
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt
index d412a40a..3bfce0be 100644
--- a/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt
@@ -6,6 +6,8 @@ import android.widget.FrameLayout
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
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.constructs.Event1
import com.futo.platformplayer.constructs.Event2
@@ -53,14 +55,30 @@ class VideoListEditorView : FrameLayout {
};
adapterVideos.onRemove.subscribe { v ->
- synchronized(_videos) {
- val index = _videos.indexOf(v);
- if(index >= 0) {
- _videos.removeAt(index);
- onVideoRemoved.emit(v);
+ val executeDelete = {
+ synchronized(_videos) {
+ val index = _videos.indexOf(v);
+ if(index >= 0) {
+ _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);
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
index 82ad7944..fcc6dc6d 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
@@ -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)) } };
}
- fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) {
- swapSources(videoSource, audioSource,false, play, keepSubtitles);
+ fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) {
+ swapSources(videoSource, audioSource,resume, play, keepSubtitles);
}
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
var videoSourceUsed = videoSource;
diff --git a/app/src/main/res/layout/activity_add_source.xml b/app/src/main/res/layout/activity_add_source.xml
index 9adf25ed..469a4508 100644
--- a/app/src/main/res/layout/activity_add_source.xml
+++ b/app/src/main/res/layout/activity_add_source.xml
@@ -22,6 +22,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
diff --git a/app/src/main/res/layout/activity_add_source_options.xml b/app/src/main/res/layout/activity_add_source_options.xml
index 28752940..541d489d 100644
--- a/app/src/main/res/layout/activity_add_source_options.xml
+++ b/app/src/main/res/layout/activity_add_source_options.xml
@@ -22,6 +22,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
diff --git a/app/src/main/res/layout/activity_dev.xml b/app/src/main/res/layout/activity_dev.xml
index 1780ede6..c1caef04 100644
--- a/app/src/main/res/layout/activity_dev.xml
+++ b/app/src/main/res/layout/activity_dev.xml
@@ -22,6 +22,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
diff --git a/app/src/main/res/layout/activity_fcast_guide.xml b/app/src/main/res/layout/activity_fcast_guide.xml
index 4d6a2b89..5ccd0fa7 100644
--- a/app/src/main/res/layout/activity_fcast_guide.xml
+++ b/app/src/main/res/layout/activity_fcast_guide.xml
@@ -10,6 +10,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
index fd35ed06..6f8fe755 100644
--- a/app/src/main/res/layout/activity_login.xml
+++ b/app/src/main/res/layout/activity_login.xml
@@ -14,6 +14,7 @@
android:id="@+id/button_close"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_close"
android:scaleType="fitCenter"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 34f54a1f..1708eeb4 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -75,6 +75,7 @@
android:id="@+id/incognito_button"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_incognito_button"
android:src="@drawable/ic_disabled_visible_purple"
android:background="@drawable/background_button_round_black"
android:scaleType="fitCenter"
diff --git a/app/src/main/res/layout/activity_manage_tabs.xml b/app/src/main/res/layout/activity_manage_tabs.xml
index 7a4a1e1f..986dfa11 100644
--- a/app/src/main/res/layout/activity_manage_tabs.xml
+++ b/app/src/main/res/layout/activity_manage_tabs.xml
@@ -21,6 +21,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
diff --git a/app/src/main/res/layout/activity_polycentric_backup.xml b/app/src/main/res/layout/activity_polycentric_backup.xml
index 3f437667..e31e8584 100644
--- a/app/src/main/res/layout/activity_polycentric_backup.xml
+++ b/app/src/main/res/layout/activity_polycentric_backup.xml
@@ -9,6 +9,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
@@ -19,6 +20,7 @@
android:id="@+id/button_help"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_button_help"
app:srcCompat="@drawable/ic_help"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/activity_polycentric_create_profile.xml b/app/src/main/res/layout/activity_polycentric_create_profile.xml
index 787e3efd..198488fd 100644
--- a/app/src/main/res/layout/activity_polycentric_create_profile.xml
+++ b/app/src/main/res/layout/activity_polycentric_create_profile.xml
@@ -9,6 +9,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
@@ -19,6 +20,7 @@
android:id="@+id/button_help"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_help"
app:srcCompat="@drawable/ic_help"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/activity_polycentric_home.xml b/app/src/main/res/layout/activity_polycentric_home.xml
index 36e206cd..a948d54b 100644
--- a/app/src/main/res/layout/activity_polycentric_home.xml
+++ b/app/src/main/res/layout/activity_polycentric_home.xml
@@ -9,6 +9,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
@@ -19,6 +20,7 @@
android:id="@+id/button_help"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_button_help"
app:srcCompat="@drawable/ic_help"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/activity_polycentric_import_profile.xml b/app/src/main/res/layout/activity_polycentric_import_profile.xml
index 992395cf..b7013f40 100644
--- a/app/src/main/res/layout/activity_polycentric_import_profile.xml
+++ b/app/src/main/res/layout/activity_polycentric_import_profile.xml
@@ -9,6 +9,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
@@ -19,6 +20,7 @@
android:id="@+id/button_help"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_help"
app:srcCompat="@drawable/ic_help"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/activity_polycentric_profile.xml b/app/src/main/res/layout/activity_polycentric_profile.xml
index e4b81d5d..d9266fd0 100644
--- a/app/src/main/res/layout/activity_polycentric_profile.xml
+++ b/app/src/main/res/layout/activity_polycentric_profile.xml
@@ -10,6 +10,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
@@ -20,6 +21,7 @@
android:id="@+id/button_help"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_button_help"
app:srcCompat="@drawable/ic_help"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -28,6 +30,7 @@
android:id="@+id/image_polycentric"
android:layout_height="80dp"
android:layout_width="80dp"
+ android:contentDescription="@string/cd_image_polycentric"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/roundedCorners_40dp"
app:srcCompat="@drawable/placeholder_profile"
diff --git a/app/src/main/res/layout/activity_polycentric_why.xml b/app/src/main/res/layout/activity_polycentric_why.xml
index 1f4a8872..63d8f19c 100644
--- a/app/src/main/res/layout/activity_polycentric_why.xml
+++ b/app/src/main/res/layout/activity_polycentric_why.xml
@@ -10,6 +10,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index b0bf134b..0c815387 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -24,6 +24,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingRight="20dp"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
diff --git a/app/src/main/res/layout/activity_sync_home.xml b/app/src/main/res/layout/activity_sync_home.xml
index 6cb4872c..0f353fb4 100644
--- a/app/src/main/res/layout/activity_sync_home.xml
+++ b/app/src/main/res/layout/activity_sync_home.xml
@@ -10,6 +10,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
diff --git a/app/src/main/res/layout/activity_sync_pair.xml b/app/src/main/res/layout/activity_sync_pair.xml
index e95f324b..e5355ecc 100644
--- a/app/src/main/res/layout/activity_sync_pair.xml
+++ b/app/src/main/res/layout/activity_sync_pair.xml
@@ -10,6 +10,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
diff --git a/app/src/main/res/layout/activity_sync_show_pairing_code.xml b/app/src/main/res/layout/activity_sync_show_pairing_code.xml
index a7d5631a..4d92980a 100644
--- a/app/src/main/res/layout/activity_sync_show_pairing_code.xml
+++ b/app/src/main/res/layout/activity_sync_show_pairing_code.xml
@@ -14,6 +14,7 @@
android:id="@+id/button_back"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
diff --git a/app/src/main/res/layout/dialog_automatic_backup.xml b/app/src/main/res/layout/dialog_automatic_backup.xml
index 3804866b..bfa7c0f6 100644
--- a/app/src/main/res/layout/dialog_automatic_backup.xml
+++ b/app/src/main/res/layout/dialog_automatic_backup.xml
@@ -27,6 +27,7 @@
android:id="@+id/button_cancel"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_button_close"
app:srcCompat="@drawable/ic_close_thin"
app:tint="#888888"
android:layout_marginEnd="30dp" />
@@ -71,6 +72,16 @@
android:singleLine="true"
android:inputType="textPassword"
android:hint="@string/backup_password" />
+
@@ -109,6 +110,7 @@
android:id="@+id/button_add"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_add"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_add"
app:tint="@color/primary"
diff --git a/app/src/main/res/layout/dialog_casting_connected.xml b/app/src/main/res/layout/dialog_casting_connected.xml
index 8b593e9d..027db929 100644
--- a/app/src/main/res/layout/dialog_casting_connected.xml
+++ b/app/src/main/res/layout/dialog_casting_connected.xml
@@ -58,6 +58,7 @@
android:id="@+id/image_device"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_image_device"
app:srcCompat="@drawable/ic_chromecast"
android:scaleType="fitCenter"
app:layout_constraintLeft_toLeftOf="parent"
@@ -197,6 +198,7 @@
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_previous"
android:scaleType="centerCrop"
android:clickable="true"
android:padding="10dp"
@@ -206,6 +208,7 @@
android:id="@+id/button_play"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_play"
android:padding="20dp"
android:scaleType="fitCenter"
android:clickable="true"
@@ -215,6 +218,7 @@
android:id="@+id/button_pause"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_pause"
android:padding="10dp"
android:scaleType="fitCenter"
android:clickable="true"
@@ -224,6 +228,7 @@
android:id="@+id/button_stop"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_stop"
android:scaleType="fitCenter"
android:padding="5dp"
android:clickable="true"
@@ -233,6 +238,7 @@
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_next"
android:clickable="true"
android:scaleType="centerCrop"
android:padding="10dp"
diff --git a/app/src/main/res/layout/dialog_update.xml b/app/src/main/res/layout/dialog_update.xml
index 051a604c..9dceccd4 100644
--- a/app/src/main/res/layout/dialog_update.xml
+++ b/app/src/main/res/layout/dialog_update.xml
@@ -23,6 +23,7 @@
android:id="@+id/update_spinner"
android:layout_width="100dp"
android:layout_height="100dp"
+ android:contentDescription="@string/cd_update_spinner"
app:srcCompat="@drawable/ic_update_animated" />
diff --git a/app/src/main/res/layout/fragment_navigation_top_bar.xml b/app/src/main/res/layout/fragment_navigation_top_bar.xml
index ab8d8f87..f7317f6a 100644
--- a/app/src/main/res/layout/fragment_navigation_top_bar.xml
+++ b/app/src/main/res/layout/fragment_navigation_top_bar.xml
@@ -11,6 +11,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingLeft="16dp"
android:paddingRight="8dp"
app:srcCompat="@drawable/ic_back_nav" />
@@ -34,6 +35,7 @@
android:id="@+id/button_cast"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_cast_button"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:paddingTop="9dp"
diff --git a/app/src/main/res/layout/fragment_overview_top_bar.xml b/app/src/main/res/layout/fragment_overview_top_bar.xml
index c87619bf..1fe9e047 100644
--- a/app/src/main/res/layout/fragment_overview_top_bar.xml
+++ b/app/src/main/res/layout/fragment_overview_top_bar.xml
@@ -10,6 +10,7 @@
android:id="@+id/app_icon"
android:layout_width="35dp"
android:layout_height="35dp"
+ android:contentDescription="@string/cd_app_icon"
android:layout_marginStart="16dp"
android:layout_marginEnd="4dp"
android:scaleType="fitCenter"
@@ -37,6 +38,7 @@
android:id="@+id/button_cast"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_cast_button"
android:paddingStart="16dp"
android:paddingEnd="12dp"
android:paddingTop="12dp"
@@ -49,6 +51,7 @@
android:id="@+id/button_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_search"
android:paddingStart="5dp"
android:paddingEnd="12dp"
android:paddingTop="11dp"
diff --git a/app/src/main/res/layout/fragment_playlists.xml b/app/src/main/res/layout/fragment_playlists.xml
index b86cfe68..cd2050b6 100644
--- a/app/src/main/res/layout/fragment_playlists.xml
+++ b/app/src/main/res/layout/fragment_playlists.xml
@@ -34,6 +34,7 @@
android:id="@+id/image_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_icon_history"
app:srcCompat="@drawable/ic_clock_white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -119,6 +120,7 @@
android:id="@+id/button_create_playlist"
android:layout_width="35dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_button_create_playlist"
app:srcCompat="@drawable/ic_add_white_16dp"
android:paddingEnd="15dp"
android:paddingStart="15dp"
diff --git a/app/src/main/res/layout/fragment_remote_playlist.xml b/app/src/main/res/layout/fragment_remote_playlist.xml
index bee1f714..42b38fb5 100644
--- a/app/src/main/res/layout/fragment_remote_playlist.xml
+++ b/app/src/main/res/layout/fragment_remote_playlist.xml
@@ -58,6 +58,7 @@
android:id="@+id/button_share"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_share"
android:background="@drawable/background_button_round"
android:gravity="center"
android:layout_marginStart="5dp"
diff --git a/app/src/main/res/layout/fragment_search_top_bar.xml b/app/src/main/res/layout/fragment_search_top_bar.xml
index f4b64615..277eb133 100644
--- a/app/src/main/res/layout/fragment_search_top_bar.xml
+++ b/app/src/main/res/layout/fragment_search_top_bar.xml
@@ -13,6 +13,7 @@
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_back"
android:paddingLeft="16dp"
android:paddingRight="16dp"
app:srcCompat="@drawable/ic_back_white_24dp" />
@@ -27,6 +28,7 @@
android:id="@+id/edit_search"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
+ android:hint="Search"
android:layout_weight="1"
android:inputType="text"
android:imeOptions="actionDone"
@@ -37,6 +39,7 @@
android:id="@+id/button_clear_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_clear_search"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:layout_gravity="right|center_vertical"
@@ -48,6 +51,7 @@
android:id="@+id/button_filter"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_filter"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scaleType="fitCenter"
diff --git a/app/src/main/res/layout/fragment_subscriptions_group.xml b/app/src/main/res/layout/fragment_subscriptions_group.xml
index a26789cc..31c8d5fa 100644
--- a/app/src/main/res/layout/fragment_subscriptions_group.xml
+++ b/app/src/main/res/layout/fragment_subscriptions_group.xml
@@ -43,6 +43,7 @@
android:id="@+id/button_delete"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_delete"
android:layout_marginLeft="5dp"
android:layout_marginRight="0dp"
android:src="@drawable/ic_trash"
@@ -56,6 +57,7 @@
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_settings"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:src="@drawable/ic_settings"
@@ -69,6 +71,7 @@
android:id="@+id/image_group"
android:layout_width="110dp"
android:layout_height="70dp"
+ android:contentDescription="@string/cd_image_group"
android:adjustViewBounds="true"
app:circularflow_defaultRadius="10dp"
android:layout_marginLeft="30dp"
@@ -90,6 +93,7 @@
+ android:layout_height="27dp"
+ android:contentDescription="@string/cd_creator_thumbnail" />
@@ -285,6 +288,7 @@
android:id="@+id/button_share"
android:layout_width="32dp"
android:layout_height="32dp"
+ android:contentDescription="@string/cd_button_share"
android:background="@drawable/background_button_round"
android:gravity="center"
android:layout_marginStart="5dp"
diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml
index 71ad3869..d5062c06 100644
--- a/app/src/main/res/layout/fragview_video_detail.xml
+++ b/app/src/main/res/layout/fragview_video_detail.xml
@@ -103,6 +103,7 @@
android:id="@+id/minimize_play"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_minimize_play"
android:padding="10dp"
android:clickable="true"
android:scaleType="fitCenter"
@@ -111,6 +112,7 @@
android:id="@+id/minimize_pause"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_minimize_pause"
android:padding="5dp"
android:scaleType="fitCenter"
android:clickable="true"
@@ -119,6 +121,7 @@
android:id="@+id/minimize_close"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_minimize_close"
android:padding="5dp"
android:scaleType="fitCenter"
android:layout_marginStart="2dp"
@@ -337,7 +340,8 @@
+ android:layout_height="35dp"
+ android:contentDescription="@string/cd_creator_thumbnail" />
diff --git a/app/src/main/res/layout/list_comment.xml b/app/src/main/res/layout/list_comment.xml
index 2141bf7f..ea2c861a 100644
--- a/app/src/main/res/layout/list_comment.xml
+++ b/app/src/main/res/layout/list_comment.xml
@@ -100,6 +100,7 @@
android:id="@+id/image_like_icon"
android:layout_width="18dp"
android:layout_height="18dp"
+ android:contentDescription="@string/cd_image_like_icon"
app:srcCompat="@drawable/ic_thumb_up" />
@@ -134,6 +136,7 @@
android:id="@+id/button_replies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_button_replies"
app:pillIcon="@drawable/ic_forum"
app:pillText="55 Replies"
android:layout_marginStart="15dp" />
diff --git a/app/src/main/res/layout/list_comment_with_reference.xml b/app/src/main/res/layout/list_comment_with_reference.xml
index 2e828255..73d53a3f 100644
--- a/app/src/main/res/layout/list_comment_with_reference.xml
+++ b/app/src/main/res/layout/list_comment_with_reference.xml
@@ -91,6 +91,7 @@
android:id="@+id/button_replies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_button_replies"
app:pillIcon="@drawable/ic_forum"
app:pillText="55 Replies"
android:layout_marginStart="15dp" />
@@ -112,6 +113,7 @@
android:id="@+id/pill_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:contentDescription="@string/cd_button_delete"
android:textColor="@color/white"
android:textSize="13dp"
android:gravity="center_vertical"
diff --git a/app/src/main/res/layout/list_creator.xml b/app/src/main/res/layout/list_creator.xml
index d14ad856..5237ccd9 100644
--- a/app/src/main/res/layout/list_creator.xml
+++ b/app/src/main/res/layout/list_creator.xml
@@ -56,6 +56,7 @@
android:id="@+id/button_subscribe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_button_subscribe"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/text_channel_metadata"
app:layout_constraintLeft_toLeftOf="parent"
@@ -65,6 +66,7 @@
android:id="@+id/platform_indicator"
android:layout_width="24dp"
android:layout_height="24dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:layout_marginTop="18dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
diff --git a/app/src/main/res/layout/list_device.xml b/app/src/main/res/layout/list_device.xml
index 874adba9..534a9f2f 100644
--- a/app/src/main/res/layout/list_device.xml
+++ b/app/src/main/res/layout/list_device.xml
@@ -10,6 +10,7 @@
android:id="@+id/image_device"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_image_device"
app:srcCompat="@drawable/ic_chromecast"
android:scaleType="fitCenter"
app:layout_constraintLeft_toLeftOf="parent"
@@ -63,6 +64,7 @@
android:id="@+id/image_loader"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_image_loader"
app:srcCompat="@drawable/ic_loader_animated"
android:layout_marginEnd="8dp"/>
diff --git a/app/src/main/res/layout/list_donation.xml b/app/src/main/res/layout/list_donation.xml
index 921cd87a..cf0a05f1 100644
--- a/app/src/main/res/layout/list_donation.xml
+++ b/app/src/main/res/layout/list_donation.xml
@@ -20,6 +20,7 @@
android:id="@+id/donation_author_image"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_donation_author_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/app/src/main/res/layout/list_history.xml b/app/src/main/res/layout/list_history.xml
index ec1d943c..88a10b81 100644
--- a/app/src/main/res/layout/list_history.xml
+++ b/app/src/main/res/layout/list_history.xml
@@ -173,6 +173,7 @@
android:id="@+id/image_trash"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_delete"
app:srcCompat="@drawable/ic_trash_18dp"
android:scaleType="fitCenter"
android:paddingTop="10dp"
diff --git a/app/src/main/res/layout/list_import_subscription.xml b/app/src/main/res/layout/list_import_subscription.xml
index 15424fc3..9d30ead2 100644
--- a/app/src/main/res/layout/list_import_subscription.xml
+++ b/app/src/main/res/layout/list_import_subscription.xml
@@ -45,7 +45,8 @@
+ android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_locked_preview.xml b/app/src/main/res/layout/list_locked_preview.xml
index 59cb602a..2413c98c 100644
--- a/app/src/main/res/layout/list_locked_preview.xml
+++ b/app/src/main/res/layout/list_locked_preview.xml
@@ -194,6 +194,7 @@
android:id="@+id/creator_thumbnail"
android:layout_width="32dp"
android:layout_height="32dp"
+ android:contentDescription="@string/cd_creator_thumbnail"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="10dp"
@@ -272,6 +273,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:scaleType="centerInside"
tools:src="@drawable/ic_peertube"/>
diff --git a/app/src/main/res/layout/list_locked_thumbnail.xml b/app/src/main/res/layout/list_locked_thumbnail.xml
index f5fe8a57..462d65d4 100644
--- a/app/src/main/res/layout/list_locked_thumbnail.xml
+++ b/app/src/main/res/layout/list_locked_thumbnail.xml
@@ -306,6 +306,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_platform_indicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="4dp" />
diff --git a/app/src/main/res/layout/list_playlist.xml b/app/src/main/res/layout/list_playlist.xml
index 3a46a6be..d51cdfc5 100644
--- a/app/src/main/res/layout/list_playlist.xml
+++ b/app/src/main/res/layout/list_playlist.xml
@@ -14,6 +14,7 @@
android:id="@+id/image_drag_drop"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_drag_drop"
app:srcCompat="@drawable/ic_dragdrop_white"
android:scaleType="fitCenter"
android:paddingTop="10dp"
@@ -116,6 +117,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
+ android:contentDescription="@string/cd_download_indicator"
app:srcCompat="@drawable/download_for_offline" />
@@ -174,6 +176,7 @@
android:id="@+id/image_trash"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_delete"
app:srcCompat="@drawable/ic_trash_18dp"
android:scaleType="fitCenter"
android:paddingTop="10dp"
diff --git a/app/src/main/res/layout/list_playlist_feed.xml b/app/src/main/res/layout/list_playlist_feed.xml
index f2f30845..14a01d39 100644
--- a/app/src/main/res/layout/list_playlist_feed.xml
+++ b/app/src/main/res/layout/list_playlist_feed.xml
@@ -41,6 +41,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_gravity="end"
diff --git a/app/src/main/res/layout/list_playlist_feed_preview.xml b/app/src/main/res/layout/list_playlist_feed_preview.xml
index 6ce90101..6be094b0 100644
--- a/app/src/main/res/layout/list_playlist_feed_preview.xml
+++ b/app/src/main/res/layout/list_playlist_feed_preview.xml
@@ -108,6 +108,7 @@
android:id="@+id/creator_thumbnail"
android:layout_width="32dp"
android:layout_height="32dp"
+ android:contentDescription="@string/cd_creator_thumbnail"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="10dp"
@@ -161,6 +162,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:scaleType="centerInside" />
diff --git a/app/src/main/res/layout/list_playlists.xml b/app/src/main/res/layout/list_playlists.xml
index 52ada168..a180d134 100644
--- a/app/src/main/res/layout/list_playlists.xml
+++ b/app/src/main/res/layout/list_playlists.xml
@@ -72,6 +72,7 @@
android:id="@+id/button_trash"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_delete"
app:srcCompat="@drawable/ic_trash"
android:padding="10dp"
android:scaleType="fitCenter"
diff --git a/app/src/main/res/layout/list_post_preview.xml b/app/src/main/res/layout/list_post_preview.xml
index e298fd88..3eb61c55 100644
--- a/app/src/main/res/layout/list_post_preview.xml
+++ b/app/src/main/res/layout/list_post_preview.xml
@@ -90,6 +90,7 @@
android:id="@+id/platform_indicator"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:scaleType="centerInside"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
@@ -157,6 +158,7 @@
android:id="@+id/image_like_icon"
android:layout_width="18dp"
android:layout_height="18dp"
+ android:contentDescription="@string/cd_image_like_icon"
app:srcCompat="@drawable/ic_thumb_up" />
@@ -202,6 +205,7 @@
android:id="@+id/image_comments"
android:layout_width="18dp"
android:layout_height="18dp"
+ android:contentDescription="@string/Replies"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
app:srcCompat="@drawable/ic_forum" />
diff --git a/app/src/main/res/layout/list_post_thumbnail.xml b/app/src/main/res/layout/list_post_thumbnail.xml
index e16ab48f..922e0f28 100644
--- a/app/src/main/res/layout/list_post_thumbnail.xml
+++ b/app/src/main/res/layout/list_post_thumbnail.xml
@@ -90,6 +90,7 @@
android:id="@+id/platform_indicator"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:scaleType="centerInside"
tools:src="@drawable/ic_peertube"
android:layout_marginEnd="8dp"
diff --git a/app/src/main/res/layout/list_source_construction.xml b/app/src/main/res/layout/list_source_construction.xml
index 8b5cf0af..0da89b04 100644
--- a/app/src/main/res/layout/list_source_construction.xml
+++ b/app/src/main/res/layout/list_source_construction.xml
@@ -24,6 +24,7 @@
android:id="@+id/image_source"
android:layout_width="35dp"
android:layout_height="35dp"
+ android:contentDescription="@string/cd_platform_indicator"
app:srcCompat="@drawable/ic_peertube"
android:scaleType="fitCenter" />
diff --git a/app/src/main/res/layout/list_source_disabled.xml b/app/src/main/res/layout/list_source_disabled.xml
index 4ef72985..3a6de99d 100644
--- a/app/src/main/res/layout/list_source_disabled.xml
+++ b/app/src/main/res/layout/list_source_disabled.xml
@@ -24,6 +24,7 @@
android:id="@+id/image_source"
android:layout_width="35dp"
android:layout_height="35dp"
+ android:contentDescription="@string/cd_platform_indicator"
app:srcCompat="@drawable/ic_peertube"
android:scaleType="fitCenter" />
diff --git a/app/src/main/res/layout/list_source_enabled.xml b/app/src/main/res/layout/list_source_enabled.xml
index 0223971f..630b2bcc 100644
--- a/app/src/main/res/layout/list_source_enabled.xml
+++ b/app/src/main/res/layout/list_source_enabled.xml
@@ -15,6 +15,7 @@
android:id="@+id/image_drag_drop"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_drag_drop"
app:srcCompat="@drawable/ic_dragdrop_white"
android:scaleType="fitCenter"
android:paddingTop="10dp"
@@ -34,6 +35,7 @@
android:id="@+id/image_source"
android:layout_width="35dp"
android:layout_height="35dp"
+ android:contentDescription="@string/cd_platform_indicator"
app:srcCompat="@drawable/ic_peertube"
android:scaleType="fitCenter" />
diff --git a/app/src/main/res/layout/list_subscription.xml b/app/src/main/res/layout/list_subscription.xml
index 6abd51c7..ffbdf4ed 100644
--- a/app/src/main/res/layout/list_subscription.xml
+++ b/app/src/main/res/layout/list_subscription.xml
@@ -14,6 +14,7 @@
android:id="@+id/creator_thumbnail"
android:layout_width="46dp"
android:layout_height="46dp"
+ android:contentDescription="@string/cd_creator_thumbnail"
android:layout_marginStart="20dp"/>
+ android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator" />
@@ -213,6 +215,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:scaleType="centerInside"
tools:src="@drawable/ic_peertube" />
@@ -230,6 +233,7 @@
android:id="@+id/button_add_to_watch_later"
android:layout_width="30dp"
android:layout_height="30dp"
+ android:contentDescription="@string/cd_button_add_to_watch_later"
android:layout_marginEnd="5dp"
android:background="@drawable/edit_text_background"
app:srcCompat="@drawable/ic_clock_white" />
diff --git a/app/src/main/res/layout/list_video_preview_nested.xml b/app/src/main/res/layout/list_video_preview_nested.xml
index c61c37e6..d321ed81 100644
--- a/app/src/main/res/layout/list_video_preview_nested.xml
+++ b/app/src/main/res/layout/list_video_preview_nested.xml
@@ -40,6 +40,7 @@
android:id="@+id/thumbnail_platform_nested"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="5dp"
@@ -173,6 +174,7 @@
android:id="@+id/creator_thumbnail"
android:layout_width="32dp"
android:layout_height="32dp"
+ android:contentDescription="@string/cd_creator_thumbnail"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="10dp"
@@ -242,6 +244,7 @@
@@ -250,6 +253,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="25dp"
android:layout_height="25dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:scaleType="centerInside"
tools:src="@drawable/ic_peertube"/>
@@ -267,6 +271,7 @@
android:id="@+id/button_add_to_watch_later"
android:layout_width="30dp"
android:layout_height="30dp"
+ android:contentDescription="@string/cd_button_add_to_watch_later"
android:layout_marginEnd="5dp"
android:background="@drawable/edit_text_background"
app:srcCompat="@drawable/ic_clock_white" />
diff --git a/app/src/main/res/layout/list_video_thumbnail.xml b/app/src/main/res/layout/list_video_thumbnail.xml
index 51806a95..367730cc 100644
--- a/app/src/main/res/layout/list_video_thumbnail.xml
+++ b/app/src/main/res/layout/list_video_thumbnail.xml
@@ -104,6 +104,7 @@
@@ -112,6 +113,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_gravity="end"
@@ -188,6 +190,7 @@
android:id="@+id/button_add_to_watch_later"
android:layout_width="wrap_content"
android:layout_height="27dp"
+ android:contentDescription="@string/cd_button_add_to_watch_later"
android:src="@drawable/ic_clock_white"
android:paddingTop="7dp"
android:paddingBottom="6dp"
diff --git a/app/src/main/res/layout/list_video_thumbnail_nested.xml b/app/src/main/res/layout/list_video_thumbnail_nested.xml
index 4ddc8a61..82018ddb 100644
--- a/app/src/main/res/layout/list_video_thumbnail_nested.xml
+++ b/app/src/main/res/layout/list_video_thumbnail_nested.xml
@@ -104,6 +104,7 @@
@@ -112,6 +113,7 @@
android:id="@+id/thumbnail_platform"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_platform_indicator"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_gravity="end"
@@ -227,6 +229,7 @@
android:id="@+id/button_add_to_watch_later"
android:layout_width="wrap_content"
android:layout_height="27dp"
+ android:contentDescription="@string/cd_button_add_to_watch_later"
android:src="@drawable/ic_clock_white"
android:paddingTop="7dp"
android:paddingBottom="6dp"
@@ -320,6 +323,7 @@
android:id="@+id/thumbnail_platform_nested"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:contentDescription="@string/cd_platform_indicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="4dp" />
diff --git a/app/src/main/res/layout/overlay_livechat.xml b/app/src/main/res/layout/overlay_livechat.xml
index 9339c9a5..3e83d8e4 100644
--- a/app/src/main/res/layout/overlay_livechat.xml
+++ b/app/src/main/res/layout/overlay_livechat.xml
@@ -66,6 +66,7 @@
android:id="@+id/button_close"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_close"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="7dp"
@@ -126,6 +127,7 @@
android:scaleType="fitCenter"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_creator_thumbnail"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="20dp"
@@ -159,6 +161,7 @@
android:id="@+id/donation_amount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:contentDescription="@string/cd_donation_amount"
android:gravity="center"
tools:text="$100" />
@@ -229,6 +232,7 @@
android:scaleType="fitCenter"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_creator_thumbnail"
android:layout_marginEnd="10dp"
android:layout_marginStart="-20dp"
android:src="@drawable/placeholder_profile" />
diff --git a/app/src/main/res/layout/overlay_topbar.xml b/app/src/main/res/layout/overlay_topbar.xml
index f7a9cae9..53ea310d 100644
--- a/app/src/main/res/layout/overlay_topbar.xml
+++ b/app/src/main/res/layout/overlay_topbar.xml
@@ -56,6 +56,7 @@
android:id="@+id/button_close"
android:layout_width="40dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_close"
android:padding="5dp"
android:src="@drawable/ic_close" />
diff --git a/app/src/main/res/layout/thumbnail_player_ui.xml b/app/src/main/res/layout/thumbnail_player_ui.xml
index a1df6ede..2b590856 100644
--- a/app/src/main/res/layout/thumbnail_player_ui.xml
+++ b/app/src/main/res/layout/thumbnail_player_ui.xml
@@ -25,6 +25,7 @@
android:id="@+id/thumbnail_player_unmute"
android:layout_width="34dp"
android:layout_height="34dp"
+ android:contentDescription="@string/cd_thumbnail_player_unmute"
android:padding="7dp"
android:layout_gravity="center_vertical"
android:background="@color/transparent"
diff --git a/app/src/main/res/layout/video_player_ui.xml b/app/src/main/res/layout/video_player_ui.xml
index e849b942..486936a6 100644
--- a/app/src/main/res/layout/video_player_ui.xml
+++ b/app/src/main/res/layout/video_player_ui.xml
@@ -14,6 +14,7 @@
android:id="@+id/button_minimize"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_minimize"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="3dp"
@@ -33,6 +34,7 @@
android:id="@+id/button_autoplay"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_autoplay"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -41,6 +43,7 @@
android:id="@+id/button_cast"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_cast_button"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -49,6 +52,7 @@
android:id="@+id/button_rotate_lock"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_rotate_lock"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -57,6 +61,7 @@
android:id="@+id/button_loop"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_loop"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -65,6 +70,7 @@
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_settings"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -75,6 +81,7 @@
android:id="@+id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_previous"
android:scaleType="centerCrop"
android:clickable="true"
android:layout_marginRight="40dp"
@@ -118,6 +125,7 @@
android:id="@+id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_next"
android:clickable="true"
android:scaleType="centerCrop"
android:padding="5dp"
@@ -131,6 +139,7 @@
android:id="@+id/button_fullscreen"
android:layout_width="55dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_fullscreen"
android:clickable="true"
app:srcCompat="@drawable/ic_expand"
app:layout_constraintRight_toRightOf="parent"
diff --git a/app/src/main/res/layout/video_player_ui_fullscreen.xml b/app/src/main/res/layout/video_player_ui_fullscreen.xml
index d4d93182..f291bc1c 100644
--- a/app/src/main/res/layout/video_player_ui_fullscreen.xml
+++ b/app/src/main/res/layout/video_player_ui_fullscreen.xml
@@ -13,6 +13,7 @@
android:id="@+id/button_minimize"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_minimize"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:scaleType="fitCenter"
@@ -61,6 +62,7 @@
android:id="@+id/button_autoplay"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_autoplay"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -69,6 +71,7 @@
android:id="@+id/button_cast"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_cast_button"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -77,6 +80,7 @@
android:id="@+id/button_rotate_lock"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_rotate_lock"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -85,6 +89,7 @@
android:id="@+id/button_loop"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_loop"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -93,6 +98,7 @@
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="50dp"
+ android:contentDescription="@string/cd_button_settings"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
@@ -103,6 +109,7 @@
android:id="@+id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_previous"
android:scaleType="centerCrop"
android:clickable="true"
android:layout_marginRight="40dp"
@@ -146,6 +153,7 @@
android:id="@+id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
+ android:contentDescription="@string/cd_button_next"
android:clickable="true"
android:scaleType="centerCrop"
android:padding="5dp"
@@ -159,6 +167,7 @@
android:id="@+id/button_fullscreen"
android:layout_width="55dp"
android:layout_height="40dp"
+ android:contentDescription="@string/cd_button_fullscreen"
android:clickable="true"
app:srcCompat="@drawable/ic_expand"
app:layout_constraintRight_toRightOf="parent"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 460d9738..6f690cb1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -82,6 +82,7 @@
Yes
No
Confirm
+ Don\'t ask again
Are you sure you want to delete this playlist?
Are you sure you want to delete this subscription?
Removing this source will result in some of your subscriptions not being resolved.
@@ -180,6 +181,7 @@
Set a password for your daily backup
Set a password used to encrypt your daily backup that is written to external storage.
Backup Password
+ Repeat Password
Restore from Automatic Backup
It appears an automatic backup exists on your device, if you would like to restore, enter your backup password.
Restore
@@ -397,6 +399,8 @@
Allow video to go underneath the screen cutout in full-screen.\nMay require restart
Enable autoplay by default
Autoplay will be enabled by default whenever you watch a video
+ Delete from WatchLater when watched
+ After you leave a video that you mostly watched, it will be removed from watch later.
Allow fullscreen portrait
Switch to Audio in Background
Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter
@@ -417,6 +421,8 @@
Payment
Payment Status
Bypass Rotation Prevention
+ Playlist Delete Confirmation
+ Show confirmation dialog when deleting media from a playlist
Enable Polycentric
Can be disabled when you are experiencing issues
Allows for rotation on non-video views.\nWARNING: Not designed for it
@@ -608,6 +614,7 @@
Do you want to convert channel {channelName} to a playlist?
Failed to convert channel
Page
+ Sync Video
Hide
Hide from Home
Hide Creator from Home
@@ -805,6 +812,57 @@
Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions.
Contribute Personal Subscriptions List
\nWould you liked to contribute your current creator subscriptions list to FUTO?\n\nThe data will be handled according to the Grayjay privacy policy. That is the list will be anonymized and stored without any reference to whomever the list of creators belonged to.\n\nThe intention is for Grayjay and FUTO to use these data to build a cross platform creator recommendation system to make it easier to find new creators you might like from within Grayjay.
+ Cast button
+ Incognito button
+ Creator thumbnail
+ Clear search
+ Search
+ Search icon
+ Back button
+ App icon
+ History icon
+ Create playlist
+ Share
+ Filter
+ Delete
+ Settings
+ Group image
+ Edit
+ Download
+ Close
+ Pause
+ Play
+ Donation amount
+ Replies
+ Like
+ Dislike
+ Subscribe
+ Platform indicator
+ Device icon
+ Loader
+ Donation author's image
+ Edit image
+ Add
+ Download indicator
+ Drag and drop
+ Add to Watch Later
+ Close
+ Unmute
+ Minimize
+ Lock rotation
+ Loop
+ Previous
+ Next
+ Fullscreen
+ Autoplay
+ Update spinner
+ Play
+ Pause
+ Stop
+ Scan QR code
+ Help
+ Change Polycentric profile picture
+ Settings
- Recommendations
- Subscriptions
diff --git a/app/src/stable/assets/sources/bilibili b/app/src/stable/assets/sources/bilibili
index 5809463f..9dedbca4 160000
--- a/app/src/stable/assets/sources/bilibili
+++ b/app/src/stable/assets/sources/bilibili
@@ -1 +1 @@
-Subproject commit 5809463f3dc2fd81fb92740ede467e271b5ca0c3
+Subproject commit 9dedbca4f27cfca2e2a146d6edb6a9bae7541d67
diff --git a/app/src/stable/assets/sources/nebula b/app/src/stable/assets/sources/nebula
index ed6e7fe3..9e6dcf09 160000
--- a/app/src/stable/assets/sources/nebula
+++ b/app/src/stable/assets/sources/nebula
@@ -1 +1 @@
-Subproject commit ed6e7fe340f2b90c3f9ad35993c5b0bf89593c29
+Subproject commit 9e6dcf093538511eac56dc44a32b99139f1f1005
diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee
index b8ceab3e..59774ac0 160000
--- a/app/src/stable/assets/sources/odysee
+++ b/app/src/stable/assets/sources/odysee
@@ -1 +1 @@
-Subproject commit b8ceab3e572be982171ceac09be7a7ad7878b8e8
+Subproject commit 59774ac08406e29f1408cb461caa5b79c805c6e1
diff --git a/app/src/stable/assets/sources/patreon b/app/src/stable/assets/sources/patreon
index 5b191993..7b66aea9 160000
--- a/app/src/stable/assets/sources/patreon
+++ b/app/src/stable/assets/sources/patreon
@@ -1 +1 @@
-Subproject commit 5b1919934d20f8c53de9959b04bdb66e0c6af3e9
+Subproject commit 7b66aea99f08303eedea879b236c49132669d2b8
diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify
index b94d5a50..75ca0c0f 160000
--- a/app/src/stable/assets/sources/spotify
+++ b/app/src/stable/assets/sources/spotify
@@ -1 +1 @@
-Subproject commit b94d5a5091ae0929d82c703868616158607a4436
+Subproject commit 75ca0c0f1e31394ec4c82d5320fa9330df849f6f
diff --git a/app/src/stable/assets/sources/twitch b/app/src/stable/assets/sources/twitch
index 58ea7722..543a727d 160000
--- a/app/src/stable/assets/sources/twitch
+++ b/app/src/stable/assets/sources/twitch
@@ -1 +1 @@
-Subproject commit 58ea77229dcdb5c9ce8f1bd642baf29486d0bf21
+Subproject commit 543a727d781fe5780fd0e8f20d53f6a53b285446
diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube
index 95ae01d5..0ce91be2 160000
--- a/app/src/stable/assets/sources/youtube
+++ b/app/src/stable/assets/sources/youtube
@@ -1 +1 @@
-Subproject commit 95ae01d5358328583fc3a3b59a2a0ca9d06301d2
+Subproject commit 0ce91be276681ab82d26f9471523beab6b2a0a00
diff --git a/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt b/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt
index 1597fd64..33b640f9 100644
--- a/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt
+++ b/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt
@@ -524,8 +524,8 @@ class NoiseProtocolTest {
println("Initiator handshake complete")
handshakeLatch.countDown() // Handshake complete for initiator
},
- onData = { session, opcode, data ->
- println("Initiator received: Opcode $opcode, Data Length: ${data.remaining()}")
+ onData = { session, opcode, subOpcode, data ->
+ println("Initiator received: Opcode: $opcode, SubOpcode: $subOpcode, Data Length: ${data.remaining()}")
when (data.remaining()) {
randomBytesExactlyOnePacket.remaining() -> {
@@ -556,8 +556,8 @@ class NoiseProtocolTest {
println("Responder handshake complete")
handshakeLatch.countDown() // Handshake complete for responder
},
- onData = { session, opcode, data ->
- println("Responder received: Opcode $opcode, Data Length: ${data.remaining()}")
+ onData = { session, opcode, subOpcode, data ->
+ println("Responder received: Opcode $opcode, SubOpcode $subOpcode, Data Length: ${data.remaining()}")
when (data.remaining()) {
randomBytesExactlyOnePacket.remaining() -> {
@@ -590,12 +590,12 @@ class NoiseProtocolTest {
responderSession.send(SyncSocketSession.Opcode.PONG.value)
// Test data transfer
- responderSession.send(SyncSocketSession.Opcode.NOTIFY_AUTHORIZED.value, randomBytesExactlyOnePacket)
- initiatorSession.send(SyncSocketSession.Opcode.NOTIFY_AUTHORIZED.value, randomBytes)
+ responderSession.send(SyncSocketSession.Opcode.DATA.value, 0u, randomBytesExactlyOnePacket)
+ initiatorSession.send(SyncSocketSession.Opcode.DATA.value, 1u, randomBytes)
// Send large data to test stream handling
val start = System.currentTimeMillis()
- responderSession.send(SyncSocketSession.Opcode.NOTIFY_AUTHORIZED.value, randomBytesBig)
+ responderSession.send(SyncSocketSession.Opcode.DATA.value, 0u, randomBytesBig)
println("Sent 10MB in ${System.currentTimeMillis() - start}ms")
// Wait for a brief period to simulate delay and allow communication
diff --git a/app/src/unstable/AndroidManifest.xml b/app/src/unstable/AndroidManifest.xml
index a30bb7af..7c47d7bd 100644
--- a/app/src/unstable/AndroidManifest.xml
+++ b/app/src/unstable/AndroidManifest.xml
@@ -38,6 +38,8 @@
+
+
@@ -67,6 +69,8 @@
+
+
diff --git a/app/src/unstable/assets/sources/bilibili b/app/src/unstable/assets/sources/bilibili
index 5809463f..9dedbca4 160000
--- a/app/src/unstable/assets/sources/bilibili
+++ b/app/src/unstable/assets/sources/bilibili
@@ -1 +1 @@
-Subproject commit 5809463f3dc2fd81fb92740ede467e271b5ca0c3
+Subproject commit 9dedbca4f27cfca2e2a146d6edb6a9bae7541d67
diff --git a/app/src/unstable/assets/sources/nebula b/app/src/unstable/assets/sources/nebula
index ed6e7fe3..9e6dcf09 160000
--- a/app/src/unstable/assets/sources/nebula
+++ b/app/src/unstable/assets/sources/nebula
@@ -1 +1 @@
-Subproject commit ed6e7fe340f2b90c3f9ad35993c5b0bf89593c29
+Subproject commit 9e6dcf093538511eac56dc44a32b99139f1f1005
diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee
index b8ceab3e..59774ac0 160000
--- a/app/src/unstable/assets/sources/odysee
+++ b/app/src/unstable/assets/sources/odysee
@@ -1 +1 @@
-Subproject commit b8ceab3e572be982171ceac09be7a7ad7878b8e8
+Subproject commit 59774ac08406e29f1408cb461caa5b79c805c6e1
diff --git a/app/src/unstable/assets/sources/patreon b/app/src/unstable/assets/sources/patreon
index 5b191993..7b66aea9 160000
--- a/app/src/unstable/assets/sources/patreon
+++ b/app/src/unstable/assets/sources/patreon
@@ -1 +1 @@
-Subproject commit 5b1919934d20f8c53de9959b04bdb66e0c6af3e9
+Subproject commit 7b66aea99f08303eedea879b236c49132669d2b8
diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify
index b94d5a50..75ca0c0f 160000
--- a/app/src/unstable/assets/sources/spotify
+++ b/app/src/unstable/assets/sources/spotify
@@ -1 +1 @@
-Subproject commit b94d5a5091ae0929d82c703868616158607a4436
+Subproject commit 75ca0c0f1e31394ec4c82d5320fa9330df849f6f
diff --git a/app/src/unstable/assets/sources/twitch b/app/src/unstable/assets/sources/twitch
index 58ea7722..543a727d 160000
--- a/app/src/unstable/assets/sources/twitch
+++ b/app/src/unstable/assets/sources/twitch
@@ -1 +1 @@
-Subproject commit 58ea77229dcdb5c9ce8f1bd642baf29486d0bf21
+Subproject commit 543a727d781fe5780fd0e8f20d53f6a53b285446
diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube
index 95ae01d5..0ce91be2 160000
--- a/app/src/unstable/assets/sources/youtube
+++ b/app/src/unstable/assets/sources/youtube
@@ -1 +1 @@
-Subproject commit 95ae01d5358328583fc3a3b59a2a0ca9d06301d2
+Subproject commit 0ce91be276681ab82d26f9471523beab6b2a0a00
diff --git a/dep/futopay b/dep/futopay
index c8992e6a..c3f532c6 160000
--- a/dep/futopay
+++ b/dep/futopay
@@ -1 +1 @@
-Subproject commit c8992e6a0ef462d11dfaf716ebe1caf46c926611
+Subproject commit c3f532c660527ae579c1dff0d2f9f4d8ea4d3173
diff --git a/images/casting.jpg b/images/casting.jpg
deleted file mode 100755
index 3a88e381..00000000
Binary files a/images/casting.jpg and /dev/null differ
diff --git a/images/casting.png b/images/casting.png
new file mode 100644
index 00000000..8f8a24a4
Binary files /dev/null and b/images/casting.png differ
diff --git a/images/channel.jpg b/images/channel.jpg
deleted file mode 100755
index 5f6d51cc..00000000
Binary files a/images/channel.jpg and /dev/null differ
diff --git a/images/channel.png b/images/channel.png
new file mode 100644
index 00000000..356631e9
Binary files /dev/null and b/images/channel.png differ
diff --git a/images/creators.png b/images/creators.png
index 71b458d9..e989eefe 100644
Binary files a/images/creators.png and b/images/creators.png differ
diff --git a/images/downloads.jpg b/images/downloads.jpg
deleted file mode 100755
index 584ab75b..00000000
Binary files a/images/downloads.jpg and /dev/null differ
diff --git a/images/downloads.png b/images/downloads.png
new file mode 100644
index 00000000..9373d54f
Binary files /dev/null and b/images/downloads.png differ
diff --git a/images/history.jpg b/images/history.jpg
deleted file mode 100755
index aaaa91d4..00000000
Binary files a/images/history.jpg and /dev/null differ
diff --git a/images/history.png b/images/history.png
new file mode 100644
index 00000000..242c1df3
Binary files /dev/null and b/images/history.png differ
diff --git a/images/playlist.jpg b/images/playlist.jpg
deleted file mode 100755
index b853216c..00000000
Binary files a/images/playlist.jpg and /dev/null differ
diff --git a/images/playlist.png b/images/playlist.png
new file mode 100644
index 00000000..6d93c640
Binary files /dev/null and b/images/playlist.png differ
diff --git a/images/playlists.jpg b/images/playlists.jpg
deleted file mode 100755
index bff89844..00000000
Binary files a/images/playlists.jpg and /dev/null differ
diff --git a/images/playlists.png b/images/playlists.png
new file mode 100644
index 00000000..349f8adf
Binary files /dev/null and b/images/playlists.png differ
diff --git a/images/search-list.jpg b/images/search-list.jpg
deleted file mode 100755
index c5c140ad..00000000
Binary files a/images/search-list.jpg and /dev/null differ
diff --git a/images/search-list.png b/images/search-list.png
new file mode 100644
index 00000000..3b0bca3d
Binary files /dev/null and b/images/search-list.png differ
diff --git a/images/search-preview.jpg b/images/search-preview.jpg
deleted file mode 100755
index e14b4d5d..00000000
Binary files a/images/search-preview.jpg and /dev/null differ
diff --git a/images/search-preview.png b/images/search-preview.png
new file mode 100644
index 00000000..c0fd0cd7
Binary files /dev/null and b/images/search-preview.png differ
diff --git a/images/search-suggestions.jpg b/images/search-suggestions.jpg
deleted file mode 100755
index 7bf7f8a5..00000000
Binary files a/images/search-suggestions.jpg and /dev/null differ
diff --git a/images/search-suggestions.png b/images/search-suggestions.png
new file mode 100644
index 00000000..aa01fe54
Binary files /dev/null and b/images/search-suggestions.png differ
diff --git a/images/settings.jpg b/images/settings.jpg
deleted file mode 100755
index 12a3b8d3..00000000
Binary files a/images/settings.jpg and /dev/null differ
diff --git a/images/settings.png b/images/settings.png
new file mode 100644
index 00000000..16378e7f
Binary files /dev/null and b/images/settings.png differ
diff --git a/images/source-install.png b/images/source-install.png
index 6e63a31b..514ba077 100644
Binary files a/images/source-install.png and b/images/source-install.png differ
diff --git a/images/source-settings.jpg b/images/source-settings.jpg
deleted file mode 100755
index 8674ce70..00000000
Binary files a/images/source-settings.jpg and /dev/null differ
diff --git a/images/source-settings.png b/images/source-settings.png
new file mode 100644
index 00000000..01281a58
Binary files /dev/null and b/images/source-settings.png differ
diff --git a/images/source.jpg b/images/source.jpg
deleted file mode 100755
index 4df7cf0f..00000000
Binary files a/images/source.jpg and /dev/null differ
diff --git a/images/source.png b/images/source.png
new file mode 100644
index 00000000..7188ac6b
Binary files /dev/null and b/images/source.png differ
diff --git a/images/sources-disabled.jpg b/images/sources-disabled.jpg
deleted file mode 100755
index db84ac83..00000000
Binary files a/images/sources-disabled.jpg and /dev/null differ
diff --git a/images/sources.jpg b/images/sources.jpg
deleted file mode 100755
index 1ce53652..00000000
Binary files a/images/sources.jpg and /dev/null differ
diff --git a/images/subscriptions-list.png b/images/subscriptions-list.png
index 62c034d6..0eb18975 100644
Binary files a/images/subscriptions-list.png and b/images/subscriptions-list.png differ
diff --git a/images/subscriptions-preview.png b/images/subscriptions-preview.png
index 6aafa9cd..9af23834 100644
Binary files a/images/subscriptions-preview.png and b/images/subscriptions-preview.png differ
diff --git a/images/video-details.jpg b/images/video-details.jpg
deleted file mode 100755
index 11d8975e..00000000
Binary files a/images/video-details.jpg and /dev/null differ
diff --git a/images/video-details.png b/images/video-details.png
new file mode 100644
index 00000000..f2a99aa4
Binary files /dev/null and b/images/video-details.png differ
diff --git a/images/video.jpg b/images/video.jpg
deleted file mode 100755
index f740a590..00000000
Binary files a/images/video.jpg and /dev/null differ
diff --git a/images/video.png b/images/video.png
new file mode 100644
index 00000000..fb3b6842
Binary files /dev/null and b/images/video.png differ