Compare commits

...

768 Commits

Author SHA1 Message Date
Koen J 547fe7bc13 Updated target SDK. 2025-09-29 15:46:45 +02:00
Koen J 678305e366 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-09-29 15:28:31 +02:00
Koen J 9f07673d85 Updated compile SDK. 2025-09-29 15:27:51 +02:00
Kelvin 19429263a9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-09-29 15:01:19 +02:00
Kelvin 986652adab Refs 2025-09-29 15:01:04 +02:00
Koen J 4d93a58d5d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-09-29 13:15:58 +02:00
Koen J 817c90f3af Translation fix. 2025-09-29 13:15:06 +02:00
Kelvin 77348b3787 Refs 2025-09-29 13:06:42 +02:00
Kelvin 31e26d03c6 Merge 2025-09-29 12:45:00 +02:00
Kelvin 1ef566ab16 Async fixes, local file playback support 2025-09-29 12:31:17 +02:00
Koen J 7597f5136c Fix Android getting stuck. 2025-09-26 13:46:43 +02:00
Koen 9a2a70622f Merge branch 'marcus/fcast-casting-sdk' into 'master'
Experimental casting backend

See merge request videostreaming/grayjay!145
2025-09-10 15:26:16 +00:00
Marcus Hanestad 4fc33411fd Experimental casting backend 2025-09-10 15:26:16 +00:00
Kelvin a9bb900994 Change when plugins are disabled on reload and listing reloads 2025-09-08 19:00:41 +02:00
Koen J 8c1a18d8b4 Build fixes. 2025-08-27 09:58:54 +02:00
Koen J 14ae5f1572 Fixed translations to align. 2025-08-26 21:32:13 +02:00
koen-futo ed40994600 Merge pull request #2357 from 0xrxL/master
Italian localization
2025-08-26 21:14:14 +02:00
koen-futo 90e8c35b19 Merge pull request #2096 from alpqn/master
Added Turkish Translations
2025-08-26 21:13:47 +02:00
Kelvin 4d017ad357 Refs 2025-08-24 21:41:07 +02:00
Kelvin 2ca2a9db23 Workaround for global lifetime scope unavailable 2025-08-24 19:35:15 +02:00
Kelvin 713d46c781 Refs 2025-08-21 22:23:07 +02:00
Kelvin 0429665173 Fix title for relay server 2025-08-21 22:18:00 +02:00
Kelvin ac05edca77 Setting to disable short filling 2025-08-21 22:14:30 +02:00
Kelvin ad3dacf68f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-08-21 22:07:15 +02:00
Kelvin 91a8996c11 Shorts fix video size scaling for some aspect ratios, long press support for tags, home plugin filters now support long press to only select that one 2025-08-21 22:06:52 +02:00
Kelvin 40c4a51a2b Dialog input support, configurable relay server, radio views select/deselect all long press 2025-08-21 20:41:18 +02:00
Kelvin f8e0aaf4d2 Merge branch 'fix-logincall' into 'master'
fix: login prompt looping on search video

See merge request videostreaming/grayjay!144
2025-08-21 16:00:35 +00:00
zvonimir ad97b5a406 fix: login prompt looping on search video 2025-08-21 17:59:08 +02:00
Kelvin b0e0c1b75f Merge branch 'PiP-play-pause-fix' into 'master'
PiP Play Pause Fix

See merge request videostreaming/grayjay!143
2025-08-20 13:41:43 +00:00
Kai b1fce443e9 fix pause and play buttons not working correctly in PiP
Changelog: changed
2025-08-20 08:39:12 -04:00
Kelvin 66f8711055 Fix login warnings working on redirects 2025-08-19 17:52:14 +02:00
Kelvin b7c123c281 Refs 2025-08-19 16:45:57 +02:00
Kelvin 9481bbf3f1 Vod chat button fix, default settings in devportal 2025-08-19 16:42:17 +02:00
Kelvin 43ec7e821b Refs 2025-08-18 21:31:49 +02:00
Kelvin ca3454afbe Login warning fixes, uimod (disabled) 2025-08-18 19:35:12 +02:00
Kelvin 1edc8aabf8 Fix login dialog 2025-08-15 21:20:23 +02:00
Kelvin 91060faac9 VOD chat 2025-08-15 16:36:38 +02:00
Kelvin 17027ba364 Remote history sync on toggle 2025-08-14 21:03:39 +02:00
Kelvin 8569eaa5db Hide DevSubmit filter 2025-08-14 20:36:56 +02:00
Kelvin d32d817e0a Merge branch 'shorts-improv' into 'master'
Fix background play, disable artwork on background till improved, renamed...

See merge request videostreaming/grayjay!140
2025-08-14 11:26:47 +00:00
Kelvin a0f4cc760c Fix background play, disable artwork on background till improved, renamed variable that caused confusion 2025-08-14 12:35:46 +02:00
Kelvin 5247997ea5 Set plugin install request timeouts, fix messaging surrounding downloading icons 2025-08-13 19:36:26 +02:00
Kelvin 453030d561 Merge branch 'shorts-improv' into 'master'
Various shorts improvements, login warnings support, etc

See merge request videostreaming/grayjay!138
2025-08-13 16:11:30 +00:00
Kelvin e080702a52 Fix dislike color 2025-08-13 17:56:27 +02:00
Kelvin 3909343adc Pre-generate support shorts, subtitle size, short like/dislike color 2025-08-13 00:23:54 +02:00
Kelvin dc76934d0e Add explicit long type for dash dwonload length 2025-08-12 17:05:54 +02:00
Kelvin 6cf47d592a Various shorts improvements, login warnings support, etc 2025-08-12 02:03:04 +02:00
Kai 1507c70729 fix https://github.com/futo-org/grayjay-android/issues/2585
Changelog: changed
2025-08-11 16:45:03 -04:00
Kai d6a23ac0de fix PiP issue
reproduction steps

- play a video
- swipe home to enter PiP
- minimize the video and then close it with the X
- swipe home (PiP will launch even though it shouldn't because nothing is playing)

Changelog: changed
2025-08-11 14:48:23 -05:00
koen-futo 17df396672 Merge pull request #2597 from alpqn/patch-1
Fix typos
2025-08-11 10:56:28 +02:00
quonverbat 0c5ba0cd39 Fix typos 2025-08-10 16:22:56 +03:00
Koen 183aeb18a0 Merge branch 'plugin-add-mixcloud' into 'master'
Add mixcloud plugin

See merge request videostreaming/grayjay!137
2025-08-07 08:00:16 +00:00
Stefan 8d08e19cd2 Add mixcloud plugin 2025-08-07 08:00:17 +01:00
Kelvin a882d04d26 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-08-01 21:56:00 +02:00
Kelvin c4d06c1ba2 Hide sync ui, thumbnails nullable 2025-08-01 21:55:47 +02:00
Kelvin 4dfcd47901 Merge branch 'fix-null-thumbnails-error' into 'master'
fix: erroring out when thumbnails are null which causes sync to reset

See merge request videostreaming/grayjay!136
2025-07-31 16:37:29 +00:00
zvonimir 4c0c1abb4b fix: erroring out when thumbnails are null which causes sync to reset 2025-07-31 18:34:24 +02:00
Kelvin 6f44071186 Merge branch 'update-docs' into 'master'
docs: add section for Request Modifiers in Content types document

See merge request videostreaming/grayjay!134
2025-07-31 14:00:17 +00:00
Stefan 29910a2698 docs: add section for Request Modifiers in Content types document 2025-07-31 14:00:17 +00:00
Kelvin b5da0d4462 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-30 18:22:27 +02:00
Kelvin 99fb9b3462 VOD chat support 2025-07-30 18:22:15 +02:00
Koen J 5f0a89d13b Implemented URLs open outside of live chat webview. 2025-07-30 13:46:46 +02:00
Koen f311561e6f Merge branch 'fix-android-anr-swap-sources' into 'master'
Fix Android ANR in SwapSources.

See merge request videostreaming/grayjay!135
2025-07-29 09:51:15 +00:00
Koen J 2fc944ddd9 Cleanup. 2025-07-29 11:15:49 +02:00
Koen J a2970b86ee Fixed issue where Scan QR button vanishes due to missing owner activity and fixed issue where remembered devices do not show until at least one normal device is found. 2025-07-29 10:58:41 +02:00
Kelvin ac9a51f105 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-29 01:39:48 +02:00
Kelvin 90dca2537a getUserHistory support 2025-07-29 01:39:32 +02:00
Koen J 4df227147c Fix Android ANR in SwapSources. 2025-07-28 15:44:37 +02:00
Koen J 1fb55dca0a On casting device disconnect, only set play when ready to true if it was also playing on the TV device. 2025-07-24 13:44:04 +02:00
Koen J 3d7b347e49 Do not call playVideo on reconnects, but instead check MEDIA_STATUS. 2025-07-24 13:05:28 +02:00
Koen J 769ec9f59a Only auto relaunch player the first time ChromeCast is started, do not reset time to 0 if player is not found, stop casting if ChromeCast player is disconnected. 2025-07-24 12:23:23 +02:00
Koen J dee310de3d Potential crashfix for downloads. 2025-07-24 11:16:48 +02:00
Koen 0af4bad906 Merge branch 'hls-url-redirect-fix' into 'master'
Nebula Download Fix

See merge request videostreaming/grayjay!129
2025-07-22 11:54:41 +00:00
Koen 4731673ba3 Merge branch 'fix-fullscreen-ui-offset' into 'master'
Fullscreen UI Fix

See merge request videostreaming/grayjay!116
2025-07-22 09:41:42 +00:00
Koen J 8745221cbd Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into fix-fullscreen-ui-offset 2025-07-22 11:39:20 +02:00
Koen 742d95440e Merge branch 'pip-improvement' into 'master'
implement the quick PiP feature

See merge request videostreaming/grayjay!107
2025-07-22 09:38:39 +00:00
Koen J 180b320cd7 Merge branch 'pip-improvement' of gitlab.futo.org:videostreaming/grayjay into pip-improvement 2025-07-22 11:31:27 +02:00
Koen J cc8dffc485 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into pip-improvement 2025-07-22 11:30:35 +02:00
Koen a64fd2cf35 Merge branch 'align-manifests' into 'master'
Align Manifests

See merge request videostreaming/grayjay!123
2025-07-21 15:07:27 +00:00
Koen 4aceb364d9 Edit AndroidManifest.xml 2025-07-21 15:06:57 +00:00
Koen J 76d9bac0ec Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-21 17:03:14 +02:00
Koen J 2b8dc41d0d Artwork should only show when audio mode is not transitioning out. 2025-07-21 17:03:00 +02:00
Koen 33430c538c Merge branch 'preview-feed-fix-constraints' into 'master'
Feed Preview Graphical Bug

See merge request videostreaming/grayjay!121
2025-07-21 14:01:37 +00:00
Koen J 03e9cb398b Made isLimitedVersion check more specific. 2025-07-21 15:54:48 +02:00
Koen J 2aef2ebec1 Background playback fixes for limited version and artwork now shows while in background playback. 2025-07-21 15:43:43 +02:00
Koen J 5e5fffbf97 loadPager should not be called on init. Small fix for restoring brightness when exiting app. 2025-07-21 14:57:03 +02:00
Koen J 51ac604e31 Various crash fixes. 2025-07-21 14:41:36 +02:00
Koen 4e49b5bc63 Merge branch 'shorts-tab' into 'master'
shorts tab

See merge request videostreaming/grayjay!92
2025-07-21 12:33:14 +00:00
Koen 658cbc5e00 Edit FCastCastingDevice.kt 2025-07-18 08:34:22 +00:00
Koen J 2ceb4c5644 Fixed issue where streams are not proxied when a request modifier is present. 2025-07-17 11:15:38 +02:00
quonverbat 940bed2cee Merge branch 'futo-org:master' into master 2025-07-12 22:12:21 +03:00
Kai 2738954af7 add hard coded padding to compensate for this bug
https://gitlab.futo.org/videostreaming/grayjay/-/merge_requests/133

Changelog: changed
2025-07-11 15:40:01 -05:00
Kai db5aaf0b84 fix delay when opening quality overlay
Changelog: changed
2025-07-11 13:50:32 -05:00
Kai e1abb7f8ae prevent scroll to top from showing when it shouldn't
fix zero state

Changelog: changed
2025-07-11 12:18:02 -05:00
Koen J 3310ac6008 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab 2025-07-11 10:16:21 +02:00
Kai 09879c83e9 fix sources button click
Changelog: changed
2025-07-10 10:45:24 -05:00
Kai 7aa8b6bc14 add zero state for shorts tab
Changelog: changed
2025-07-10 10:40:52 -05:00
Kai cac8a8fde4 add back button when in channel shorts
hide refresh button when in channel shorts

prevent main player being open when viewing shorts

show info toast when long pressing refresh button

switch bottom bar button ids back to their original values

Changelog: changed
2025-07-10 09:30:42 -05:00
Kelvin 01cb544dfd Dont lock clients when disabling 2025-07-07 16:42:37 +02:00
Kelvin b9239b6177 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 16:18:35 +02:00
Kelvin 96ca3f62a2 Missing invokev8 wrappers 2025-07-07 16:18:23 +02:00
Koen J 73ad783881 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 16:12:33 +02:00
Koen J 3bfcf65535 Fixes to reload required exception handling for casting. 2025-07-07 16:12:03 +02:00
Kelvin 8b3b27a2a8 Stop pagers silently if the underlying object is closed 2025-07-07 16:04:19 +02:00
Kelvin a4d4835a89 Reduice font size 2025-07-07 14:26:17 +02:00
Kelvin 56c0f7bfaf Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 14:14:51 +02:00
Kelvin 736424ae35 Refs 2025-07-07 14:14:39 +02:00
Koen J 37dc778009 Fixed casting. 2025-07-07 12:45:45 +02:00
Koen J cd3cea58a4 Fixed race condition when awaiting and changing video source.. 2025-07-07 10:52:42 +02:00
Koen J 8b53e9e5e3 Processed last feedback on minigame. 2025-07-05 18:09:49 +02:00
Koen J 08e98b089c Improvements to target tap loader game. 2025-07-05 17:32:31 +02:00
Koen J 5528d71da8 Show score toast. 2025-07-05 14:07:49 +02:00
Koen J 83f520ca44 Further fixes to TargetTApLoaderView. 2025-07-05 13:47:48 +02:00
Koen J cc247ce634 Attempt at a loader game. 2025-07-05 12:58:33 +02:00
Kelvin c6caa59a90 Refs 2025-07-04 18:00:48 +02:00
Kelvin 00e28b9ce0 Better raid messaging, loader autochange to indeterminate, refs 2025-07-04 17:27:35 +02:00
Kelvin 334f58979a Add missing file 2025-07-04 16:13:11 +02:00
Kelvin 940bf163da Progress bar color, refs 2025-07-04 15:58:46 +02:00
Kelvin 2bbe0e6133 Substitute v8 object calls to wrapper function 2025-07-04 15:24:19 +02:00
Kelvin 861f34a287 Refs 2025-07-04 15:02:01 +02:00
Koen J 82f214f155 Merge branch 'shorts-tab' of gitlab.futo.org:videostreaming/grayjay into shorts-tab 2025-07-04 08:16:34 +02:00
Koen J 4ee127fe13 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab 2025-07-04 08:16:12 +02:00
Koen J 86a4cf8d84 Implemented FutoVideoPlayer loader. 2025-07-04 08:09:24 +02:00
Kelvin 2c463dd5a1 Merge branch 'wip-async' into 'master'
Basic Async support

See merge request videostreaming/grayjay!132
2025-07-03 17:19:27 +00:00
Kelvin ed3820bec0 Finish async generates 2025-07-03 19:15:14 +02:00
Kelvin 542a7f212d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into wip-async 2025-07-03 19:05:38 +02:00
Kelvin 8fb0826d69 WIP rewrite generate to async 2025-07-03 19:05:13 +02:00
Koen J deeaa55f56 Changed default behavior for isOutgoing. 2025-07-03 14:40:27 +02:00
Koen J 5b954727a1 Implemented incoming and outgoing raids. 2025-07-03 14:30:15 +02:00
Koen fae77c1a63 Merge branch 'rgba-colors' into 'master'
RGBA colors.

See merge request videostreaming/grayjay!131
2025-07-03 07:44:19 +00:00
Kelvin b69402dfe9 WIP Async support for Android 2025-07-03 00:44:54 +02:00
Koen J 1f3e306a59 RGBA colors. 2025-07-02 17:59:13 +02:00
Koen J a9605118fb Clip to outline does not make sense for a ShapeableImageView. 2025-07-01 10:02:06 +02:00
Kelvin d22e918273 Missing catches 2025-06-26 16:17:17 +02:00
Kelvin bdcb94055a Refs 2025-06-26 15:13:41 +02:00
Kelvin d0644d39da Theoretical fix for networked file import 2025-06-26 15:01:18 +02:00
Kelvin 8f3f776e22 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-26 14:35:30 +02:00
Kelvin 548752e240 missing lock 2025-06-26 14:35:00 +02:00
Koen J 7f20250951 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-24 11:43:59 +02:00
Koen J 4d720b1d81 Fixed app freezing when exporting Polycentric Identity #2405 2025-06-24 11:43:40 +02:00
Kai 1e4aefb7d5 fix https://github.com/futo-org/grayjay-android/issues/2386
Changelog: changed
2025-06-20 12:00:56 -05:00
Kai 2a825a9f83 platform icon
fix text shadow

Changelog: changed
2025-06-20 09:55:39 -05:00
Kelvin K a8921a1aba Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-20 15:35:30 +02:00
Kelvin K edb9eda0a9 Improved locking 2025-06-20 15:35:02 +02:00
Koen J 3a81676447 Fixed crash #2389. 2025-06-20 10:47:10 +02:00
Kai 6695774037 icon updates
Changelog: changed
2025-06-19 11:14:00 -05:00
Koen J 03132ff77b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-19 11:23:28 +02:00
Koen J 49ddecdea4 Potential crashfix #2382. 2025-06-19 11:21:46 +02:00
Kai a10bc8c7de fix very wide screen videos enter PiP mode
Changelog: changed
2025-06-18 12:12:39 -05:00
Kai c1e6e401cc formatting
Changelog: changed
2025-06-18 09:36:31 -05:00
Kelvin K 44ff951ec6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 16:22:51 +02:00
Kelvin K 11319e0ec5 Refs 2025-06-18 16:22:28 +02:00
Koen J 100e98a960 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 15:43:45 +02:00
Koen J c6100ede70 Added disable for hold playback rate increase. 2025-06-18 15:43:12 +02:00
Kelvin K a2986a72bd Refs 2025-06-18 14:43:20 +02:00
Kelvin K e0e90c5f74 submodules 2025-06-18 14:33:07 +02:00
Kelvin K 11992af81b Hide duration if unknown 2025-06-18 14:27:20 +02:00
Koen J 15d771f7fc Fixed channel loader not being animated. 2025-06-18 13:43:50 +02:00
Kelvin K 5ede474253 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 12:41:43 +02:00
Kelvin K 7922aa6f80 Log on busy on main 2025-06-18 12:41:21 +02:00
Kelvin K 0c1333fa15 Downgrade v8, revert comments on diff thread 2025-06-18 12:40:25 +02:00
Koen J 53b9ba0368 Reverted changes. 2025-06-18 10:29:12 +02:00
Koen J c3a8877796 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 10:07:29 +02:00
Koen J a464ae9df5 Added missing loader causing crash. 2025-06-18 10:07:02 +02:00
Kai 98b6213886 remove layout changed listener
Changelog: changed
2025-06-17 15:26:27 -05:00
Kai b6671c653c add text shadow
add HQ icon for quality settings

Changelog: changed
2025-06-17 13:45:15 -05:00
Kai 55d042bee3 add platform logo
Changelog: changed
2025-06-17 12:07:29 -05:00
Kelvin K 0d16dd0006 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-17 17:28:17 +02:00
Kelvin K 48a96140a7 isBusy checks and locking improvements 2025-06-17 17:28:10 +02:00
Kelvin 603ef8f295 Merge branch 'fix-timeout-locking' into 'master'
fix: timeoutMap being deadlocked

See merge request videostreaming/grayjay!126
2025-06-17 15:26:51 +00:00
zvonimir ab07288ba0 fix: timeoutMap being deadlocked 2025-06-17 17:25:34 +02:00
Kelvin K c0bbe5d491 Additional locking 2025-06-17 15:21:46 +02:00
Kelvin K b953ff21e7 Lock on subtitle fetch 2025-06-17 11:52:26 +02:00
Kelvin K c14378b534 Improved V8 locking, comment section on diff thread than video, global mapping of v8runtimes to plugins 2025-06-17 11:45:02 +02:00
Kai 80034ad131 update refresh text
Changelog: changed
2025-06-16 13:25:02 -05:00
Kelvin K 33d3d9a29c Improved locking 2025-06-16 19:30:52 +02:00
Kelvin K 7e83793586 Submods 2025-06-16 18:34:37 +02:00
Kelvin K 6ba9ec8bc2 Clearer name setting 2025-06-16 17:56:04 +02:00
Kelvin 0b02ab0e2d Merge branch 'plugin-fixes' into 'master'
V8 Update, V8 interaction locking, Package fixes, ReloadRequiredException support

See merge request videostreaming/grayjay!125
2025-06-16 15:48:01 +00:00
Kelvin K ff531b5e77 Cleanup, fixes, clearCookies support on httpClients 2025-06-16 17:46:00 +02:00
Kelvin K b3f9de3b83 edgecase fix 2025-06-16 14:23:34 +02:00
Kelvin K 86bd71b89c Fix edgecase 2025-06-16 14:19:23 +02:00
Kelvin K 2fca7e9a01 Locking of most known v8 interactions, fix returning previously returned jvm objects, Related fixes 2025-06-16 14:13:47 +02:00
Koen 2cc873ef60 Merge branch 'quality-selector-fix' into 'master'
fix graphical glitches with quality selector

See merge request videostreaming/grayjay!109
2025-06-16 10:07:16 +00:00
Koen 7a66ce6bcd Merge branch 'sources-tab-scrolling-fix' into 'master'
Sources Scrolling Fix

See merge request videostreaming/grayjay!114
2025-06-16 10:01:45 +00:00
Koen 2730569b6b Merge branch 'tablet-landscape-fix' into 'master'
Tablet Landscape Fix

See merge request videostreaming/grayjay!115
2025-06-16 09:57:57 +00:00
Koen ede5c4409c Merge branch 'watch-later-add-feature' into 'master'
Water Later Add Feature

See merge request videostreaming/grayjay!117
2025-06-16 09:54:43 +00:00
Koen 0dbe398435 Merge branch 'hls-quality-sort' into 'master'
Adaptive Quality Sort

See merge request videostreaming/grayjay!118
2025-06-16 09:41:22 +00:00
Koen J bcab3bccbc Fixed crash when signature fields are wrongly populated. 2025-06-16 10:43:57 +02:00
Kelvin K 58c9aeb1a2 WIP: V8 update, package http fixes, ReloadRequiredException support, other fixes. Currently broken in situations where setTimeout is used 2025-06-14 15:51:31 +02:00
0xrxL 4eb20a1843 Typo 2025-06-14 15:29:49 +02:00
0xrxL 98c6378148 Fix spacing 2025-06-14 09:04:40 +02:00
0xrxL bb066a7a31 Added italian localization 2025-06-14 09:02:25 +02:00
0xrxL b5d3261f03 Add files via upload 2025-06-14 09:01:41 +02:00
Kelvin K 4702787784 WIP 2025-06-13 17:47:22 +02:00
Kai 30c41044da Merge branch 'master' into pip-improvement
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
2025-06-13 08:45:09 -05:00
Kai e369676808 remove spotify.com because those are not handled by the spotify plugin
Changelog: changed
2025-06-12 14:17:56 -05:00
Kai 2fa9e65bee align stable and unstable manifests
Changelog: changed
2025-06-12 14:13:08 -05:00
Kai cf96bd1ec0 Add functionality to open channel shorts in shorts fragment
Changelog: changed
2025-06-12 14:01:11 -05:00
Kai 1f5a069877 Fix like button checked when not logged into polycentric
Open video details when tapping title

Changelog: changed
2025-06-12 10:42:08 -05:00
Koen J 13100dc38d Minor fix in playback speed setting. 2025-06-12 11:21:00 +02:00
Koen J 5227041398 Added setting for hold playback speed increase. Implemented chromecast playback rate adjustment in range [1, 2]. Implemented hold playback speed increase pill. 2025-06-12 10:33:05 +02:00
Kelvin 8491d4da1a Merge branch 'fix-ump-downloads' into 'master'
Revert downloads patch which broke downloads

See merge request videostreaming/grayjay!122
2025-06-11 16:41:20 +00:00
zvonimir 9bea1563ca Revert downloads patch which broke downloads 2025-06-11 18:36:05 +02:00
Kai adc5013ea4 Fix the constraints on the feed preview items
Changelog: changed
2025-06-11 10:30:48 -05:00
Koen J 9e7b936663 Implemented hold to play video at 2x speed gesture. 2025-06-11 17:03:53 +02:00
Kelvin 19c84475db Hotfix playback speed for non-dot locales 2025-06-10 23:27:01 +02:00
Kai 515c5e00e9 Fix live stream PiP mode
Prevent splash screen when opening PiP mode

Changelog: changed
2025-06-10 15:27:14 -05:00
Kelvin 4164b1a3f8 Build fix 2025-06-10 19:25:43 +02:00
Kelvin a9dc038190 Build fix 2025-06-10 19:20:50 +02:00
Kelvin 2825db88a5 Minor playback tracker fix, submodules 2025-06-10 18:56:19 +02:00
Kelvin 363099b303 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-10 17:39:44 +02:00
Kelvin 5e25a5054f Increase max comment length, Fix raw dash downloads ending too early, Fix playback tracker not working for downloaded videos 2025-06-10 17:33:14 +02:00
Kelvin 2bc6127f6b Merge branch 'copy-title' into 'master'
Copy Title

See merge request videostreaming/grayjay!110
2025-06-10 14:41:49 +00:00
Kelvin 064824aedf Merge branch 'copy-playlists' into 'master'
Clone Playlist

See merge request videostreaming/grayjay!111
2025-06-10 14:41:07 +00:00
Kai DeLorenzo 52044edb2e Merge branch 'brightness-fix' into 'master'
Dim Fullscreen Fix

See merge request videostreaming/grayjay!119
2025-06-10 14:38:01 +00:00
Kai fb12073a82 Only save brightness on resume fullscreen if use system brightness is enabled
Changelog: changed
2025-06-10 09:18:28 -05:00
Kai 9944842a2f Change adaptive streaming (HLS and Dash) quality to sort in descending quality to align with YouTube and the rest of Grayjay
Changelog: changed
2025-06-09 17:02:55 -05:00
Kai 99dc50894c update text
Changelog: changed
2025-06-09 16:54:24 -05:00
Kelvin de39451f67 Merge 2025-06-07 16:44:57 +02:00
Kelvin 8f28653b28 Fix edgecases for new playback speed control 2025-06-07 16:44:20 +02:00
Kai 6598dff6df add add to watch later setting
add https://github.com/futo-org/grayjay-android/issues/2173

Changelog: added
2025-06-06 23:35:59 -05:00
Kai 389798457b navigate to playlist screen after copying
Changelog: changed
2025-06-06 15:57:09 -05:00
Kai ba9f843368 fix https://github.com/futo-org/grayjay-android/issues/2165
Changelog: changed
2025-06-06 15:45:12 -05:00
Kai 623c47fa2e fix https://github.com/futo-org/grayjay-android/issues/2210
Changelog: changed
2025-06-06 15:25:46 -05:00
Kai 19861fe812 fix https://github.com/futo-org/grayjay-android/issues/2316
Changelog: changed
2025-06-06 13:40:20 -05:00
Kai dd1c04bea1 make the copied playlist name unique
Changelog: changed
2025-06-06 09:39:09 -05:00
Kelvin e6159117f6 Merge branch 'fix-scope-issue' into 'master'
fix: Scope getting removed when switching between settings 'Kelvin approved'

See merge request videostreaming/grayjay!113
2025-06-06 13:42:49 +00:00
zvonimir 0d9e1cd3c5 fix: Scope getting removed when switching between settings 'Kelvin approved' 2025-06-06 15:40:43 +02:00
Koen J 10753eb879 Sort to prefer ipv4 over ipv6. 2025-06-06 12:25:25 +02:00
Koen J 29aec21095 Merge branch 'hotfix-250606' of gitlab.futo.org:videostreaming/grayjay 2025-06-06 12:17:49 +02:00
Koen J a810f82ce2 Added boolean setting to allow link local casting over ipv4. 2025-06-06 11:21:41 +02:00
Koen J 2c454a0ec5 Added boolean setting to allow link local casting over ipv4. 2025-06-06 11:20:04 +02:00
Koen J d3dca00482 Merge branch 'hotfix-250606' of gitlab.futo.org:videostreaming/grayjay 2025-06-06 11:12:58 +02:00
Koen J d08dffd9e2 Added potential fix for having to restart app to get casting devices to show. Added persistent ordering for creators. 2025-06-06 11:12:31 +02:00
Koen J 5b50ac926e Freeze fix when clicking link in description. 2025-06-06 10:17:40 +02:00
Koen J 57a3be35d0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-06 10:17:03 +02:00
Koen J 70f36e69e6 Freeze fix when clicking link in description. 2025-06-06 10:15:15 +02:00
Kai 8e70f1b865 add long tap to copy playing video title
Changelog: added
2025-06-05 23:14:03 -05:00
Kai f86fb0ee44 add functionality to copy playlists
fix https://github.com/futo-org/grayjay-android/issues/2306

Changelog: added
2025-06-05 23:13:05 -05:00
Kelvin fe0aac7c6e WIP playback speed additions 2025-06-05 22:47:45 +02:00
Kelvin b93447f712 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-05 19:12:10 +02:00
Kelvin 84a5103526 Use lifecycle scope instead of root scope 2025-06-05 19:11:55 +02:00
Kai c333300906 fix graphical glitches with quality selector
Changelog: changed
2025-06-05 11:08:19 -05:00
Koen c94c2721d7 Revert "prevent the user from needing to tap update on system dialog when self updating"
This reverts commit a1d460385d
2025-06-05 15:14:31 +00:00
Koen J 0428c1191a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-05 15:19:00 +02:00
Koen J 8208f92802 Added view license in settings. 2025-06-05 15:18:45 +02:00
Koen 3d33c4b8e0 Merge branch 'github-issues-template' into 'master'
Improve android issue templates

See merge request videostreaming/grayjay!108
2025-06-05 12:22:49 +00:00
zvonimir d3210ec12a Improve android issue templates 2025-06-05 14:15:00 +02:00
Koen J c959b973fc Crashfix related to PiP #2041. 2025-06-05 13:17:15 +02:00
Koen J 40c195d4a0 Crashfix on stopping StateSync #2302 2025-06-05 13:14:57 +02:00
Koen J f4f1470153 Increased connect timeout. 2025-06-05 10:58:32 +02:00
Koen J 401999b5ea Fixed exception in sync. 2025-06-05 10:45:36 +02:00
Koen J 7b53315046 Another fix for connection robustness. 2025-06-05 10:38:49 +02:00
Koen J 4d170db5e0 Improvements to connection publishing for sync. 2025-06-05 10:31:13 +02:00
Koen J fa8d175101 Fixed issue in base64 encoding. 2025-06-05 09:58:10 +02:00
Koen J cbef605f22 Updated plugins. 2025-06-05 08:57:33 +02:00
Koen J cf95791dcc Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-05 08:53:19 +02:00
Koen J 919567dbdb Made sync dialogs more robust. 2025-06-05 08:52:59 +02:00
Kai DeLorenzo 8ca317a38a Merge branch 'playback-stutter-fix' into 'master'
background playback stutter fix

See merge request videostreaming/grayjay!103
2025-06-04 20:44:09 +00:00
Kelvin ccc686ed50 Downloads size ordering, Subsgroup removal on unsubscribe, multi-key like querying 2025-06-04 21:26:41 +02:00
Kelvin e3e7b0c345 More advanced settings 2025-06-04 20:50:10 +02:00
Kelvin 5b0f359944 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-04 20:43:59 +02:00
Kelvin 29f1bef099 Advanced settings option, Playlist id saving for exports and backups, Sync synchronization to prevent dups 2025-06-04 20:43:37 +02:00
Kai 0653f88c49 Merge branch 'master' into shorts-tab
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
2025-06-04 13:03:40 -05:00
Kai 4ce9f64808 make code review changes
switch to TaskHandler
switch to XML

Changelog: changed
2025-06-04 12:59:48 -05:00
Koen J 418f34c7e8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-04 15:13:42 +02:00
Koen J 21c2ab21b2 Disable drag long press for search playlists. 2025-06-04 15:13:25 +02:00
Kelvin 1ace7318f3 Submodules 2025-06-04 13:17:31 +02:00
Koen J 48052b88db Made task handler and retry dialogs more robust. 2025-06-04 13:00:32 +02:00
Koen J 715c60dc6e Fixed Chromecast position not updating on Grayjay side. Fixed Chromecast not reconnecting properly. Fixed AirPlay/Chromecast position not being reflected in history. 2025-06-04 12:13:18 +02:00
Koen J 916d052688 Updated submodules. 2025-06-04 11:04:00 +02:00
Koen J 993b812c3b Removed drag handles when searching in playlist. 2025-06-04 10:44:00 +02:00
Koen J 43887586b5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-30 18:39:45 +02:00
Koen J 03d53f21a3 Tab changes no longer fill up history. 2025-05-30 18:39:28 +02:00
Kelvin 23d7e8e5b6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-30 17:20:28 +02:00
Kelvin cce117c585 Article header support, socket handling improvements 2025-05-30 17:20:11 +02:00
Koen J 303bd1b805 Fixed spacing issue in bottom bar when button sizes vary. Potential fix for crash that happens in ShapeableImageView. 2025-05-30 14:11:05 +02:00
koen-futo c7f4a40342 Merge pull request #2288 from quonverbat/typo-fix
Fixed typo
2025-05-30 13:50:56 +02:00
quonverbat 208c6c0776 Fixed typo 2025-05-30 00:50:22 +03:00
quonverbat 755bebaecb Merge branch 'futo-org:master' into master 2025-05-30 00:07:27 +03:00
Kai 4fa0229ccb implement the quick PiP feature
https://developer.android.com/develop/ui/views/picture-in-picture#setautoenterenabled

Changelog: changed
2025-05-29 12:38:37 -05:00
Kelvin 7d5c8347ce Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-29 18:37:29 +02:00
Kelvin bd70131252 Wrapper fix for unsupported enum parameters 2025-05-29 18:37:12 +02:00
Kelvin 43a373eceb Merge branch 'article-web-support' into 'master'
Article/web support

See merge request videostreaming/grayjay!106
2025-05-29 16:08:04 +00:00
Kelvin 5bb3466ffe Web support, additional article support 2025-05-29 18:02:55 +02:00
Kai 75e97ed008 Merge remote-tracking branch 'origin/master' 2025-05-29 10:24:51 -05:00
Kai ee28604c11 switch audio HLS to m4a
Changelog: changed
2025-05-29 10:24:23 -05:00
Kelvin a7d89e1bfb Missing files 2025-05-29 16:51:31 +02:00
Kelvin cbfd9ea559 Article support 2025-05-29 16:51:06 +02:00
Koen J dae50c3bc3 Implemented navigateWhenReady for url handling. 2025-05-29 16:07:14 +02:00
Koen J e651e59dc4 Theoretical crashfix for the case where activity does not successfully resume before handleUrl is invoked #2245. 2025-05-29 15:39:46 +02:00
Koen J 80d78761bf Made chromecast more robust. Improvements to IPv6 handling of casting devices. 2025-05-29 15:17:26 +02:00
Kelvin fb85aa4f32 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into article-web-support 2025-05-29 13:05:09 +02:00
Kelvin 9635c95efe WIP article 2025-05-29 13:05:00 +02:00
Koen J 033a237488 Changed getLocalUrl logic. 2025-05-29 13:03:51 +02:00
Koen J ec22c58822 Made a fix for ui mode changes causing app restarts. 2025-05-29 11:58:10 +02:00
Koen J 274942b5ba Hide search for any tab that isn't videos. 2025-05-29 11:55:21 +02:00
Koen J 94ab3da0e4 Added separate error status code for transport rejection. Added unhandled exception handler for relay loop. Added additional booleans to keep track of the server/relay connections being up/down. Added additional messaging to let the user know when something is wrong. 2025-05-29 11:51:59 +02:00
Kai DeLorenzo 5d44f0f2b6 Merge branch 'download-encrypted-hls' into 'master'
Encrypted HLS Download

See merge request videostreaming/grayjay!89
2025-05-29 04:20:54 +00:00
Kai f051e6b452 Merge branch 'master' into download-encrypted-hls
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
2025-05-28 23:20:22 -05:00
Kai DeLorenzo 46a4284253 Merge branch 'hls-audio-fixes' into 'master'
HLS audio download

See merge request videostreaming/grayjay!86
2025-05-29 04:17:55 +00:00
Kai 0a708c6892 Merge branch 'master' into hls-audio-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
2025-05-28 23:12:50 -05:00
Kai DeLorenzo 0f96164dc3 Merge branch 'small-hls-fixes' into 'master'
Fix HLS downloading for nebula

See merge request videostreaming/grayjay!85
2025-05-29 04:10:06 +00:00
Kai 91c4917021 Merge branch 'master' into download-encrypted-hls
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
2025-05-28 22:55:09 -05:00
Kai c32ebe016b fix extension
Changelog: changed
2025-05-28 17:33:35 -05:00
Kai ea26eefc2d Merge branch 'master' into hls-audio-fixes 2025-05-28 17:24:32 -05:00
Kai 418f4a6075 Merge branch 'master' into small-hls-fixes 2025-05-28 16:57:57 -05:00
Koen J 0ec921709a Fixed search visibility and channel loader when changing query. 2025-05-28 20:32:09 +02:00
Koen J e0811cfd93 Implemented new channel search. 2025-05-28 20:08:22 +02:00
Kai DeLorenzo f6b0778eb6 Merge branch 'ffmpeg-build-fix' into 'master'
add smart exception dependency for usage by local ffmpegkit dependency

See merge request videostreaming/grayjay!105
2025-05-27 19:06:50 +00:00
Kai 18aec34c0e add smart exception dependency for usage by local ffmpegkit dependency
Changelog: added
2025-05-27 14:05:51 -05:00
Kelvin bd185776e7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into article-web-support 2025-05-27 15:49:27 +02:00
Kelvin fca5fe38bb WIP Article/WEb 2025-05-27 15:49:20 +02:00
koen-futo 1c2c7b376d Merge pull request #2152 from deenle/master
Added adaptive monochrome launcher icon
2025-05-27 11:47:33 +02:00
Koen J 670df86114 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-27 11:47:08 +02:00
Koen 55fb4d4562 Merge branch 'add-crunchyroll' into 'master'
Add Crunchyroll

See merge request videostreaming/grayjay!104
2025-05-26 12:59:15 +00:00
Koen J c703d018bd Fixed issue where loop video would not reset when opening a playlist, causing the video to loop without being able to disable it. 2025-05-26 14:22:49 +02:00
Stefan 425a27e130 Add Crunchyroll 2025-05-26 00:47:54 +01:00
Kai bd1b0e875b Merge branch 'master' into hls-audio-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
#	app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
2025-05-22 15:04:15 -05:00
Kai 1509c11f64 check to see if an HLS playlist is a master playlist before parsing it
Changelog: changed
2025-05-22 15:00:34 -05:00
Kai 57c1097fbc Merge branch 'master' into small-hls-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
2025-05-22 14:02:28 -05:00
Kai 1d1728b92b switch audio hls to be an mp4 file
Changelog: changed
2025-05-22 13:44:50 -05:00
Kai 8202513993 fix stutter when switching to background
Changelog: changed
2025-05-22 12:12:34 -05:00
Kai DeLorenzo 5f9f6dbde8 Merge branch 'linked-channel-shorts-fix' into 'master'
Polycentric Channel Shorts Tab Fix

See merge request videostreaming/grayjay!100
2025-05-22 16:32:27 +00:00
Kai cc3639180b Merge branch 'master' into linked-channel-shorts-fix
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
2025-05-22 11:31:05 -05:00
Kai 8aa4de7522 make sure the plugin supports shorts content
Changelog: added
2025-05-22 11:28:59 -05:00
Koen J ed1f7e7c72 Button incognito is now visible but translated when minimized. 2025-05-22 15:57:12 +02:00
Koen J 1ecd1f5e04 Fix for throttled networks (airplane wifi) freezing app opening downloaded content. 2025-05-22 13:45:57 +02:00
Koen J 1aa9adc899 Build fix. 2025-05-22 11:41:16 +02:00
Koen J f8b2da93b9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-22 09:36:16 +02:00
Koen J b794ff47ef Added dependency. 2025-05-22 09:34:32 +02:00
Kai DeLorenzo 6962a0547a Merge branch 'fix-get-replies' into 'master'
Fix getReplies in source.js

See merge request videostreaming/grayjay!102
2025-05-21 21:21:38 +00:00
Kai b906c1d36b add assignment for getReplies to PlatformComment
Changelog: added
2025-05-21 16:20:35 -05:00
Koen J af337b1874 Added missing checks for polycentric enabled. 2025-05-19 15:37:39 +02:00
Koen J 542235cca0 Keep live chat open when minimizing. #2227 2025-05-19 11:45:45 +02:00
Koen J f5673425b7 Updated default price. 2025-05-17 08:54:40 +02:00
Koen J 94965cf3ba Use snapshots for sending order to improve send efficiency. 2025-05-16 12:34:48 +02:00
Koen J 120ded5274 Prepared Sync logic to be separated from the rest of the logic. 2025-05-16 12:11:41 +02:00
Koen J 705eb6a3fa Migrated service to SyncService. 2025-05-15 19:57:31 +02:00
Koen J 1eb62b31d2 Added client sided ping loops to prevent inactive channels. 2025-05-13 10:31:30 +02:00
koen-futo b145187fa8 Merge pull request #1803 from ajp-dev/patch-1
fix link typo for Script Signing
2025-05-12 09:47:51 +02:00
koen-futo 4da1e44fd1 Merge pull request #1724 from AlexandrePicavet/feat/player-seek-offset
feat(player): Add a setting to adjust player seek duration
2025-05-12 09:43:22 +02:00
koen-futo 4e70279982 Merge pull request #2136 from buzzcola3/master
Fix "previous song" button playing always from unshuffled queue, even when shuffle is used
2025-05-12 09:36:52 +02:00
koen-futo 233c8ee26e Merge pull request #2155 from realchrisolin/patch-1
Update AndroidManifest.xml
2025-05-12 09:24:28 +02:00
Koen 875adb4d79 Merge branch 'fix-other-cookie-handling' into 'master'
Pass httponly cookies to future requests

See merge request videostreaming/grayjay!88
2025-05-12 07:01:41 +00:00
Koen 456514c4d4 Merge branch 'hls-rumble-fix' into 'master'
Rumble Download HLS Fix

See merge request videostreaming/grayjay!95
2025-05-12 06:46:55 +00:00
Koen J dac1918b95 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-12 08:43:12 +02:00
Koen J 1d7429ad86 Fixed issue where private mode would not re-appear after closing the video. 2025-05-12 08:43:02 +02:00
Kai DeLorenzo 5d0e6615ab Edit VideoDownload.kt 2025-05-09 17:36:27 +00:00
Kai DeLorenzo dc415df8c0 Edit VideoDownload.kt 2025-05-09 17:35:36 +00:00
Kai DeLorenzo 45ce251c4c Merge branch 'streamline-self-update' into 'master'
Silent Self Update

See merge request videostreaming/grayjay!81
2025-05-09 17:10:47 +00:00
Koen 2bc702112f Merge branch 'fix-brightness-restore' into 'master'
Brightness Restore On App Close

See merge request videostreaming/grayjay!99
2025-05-09 16:18:41 +00:00
Kai DeLorenzo abd73bf797 Merge branch 'theme-change-restart' into 'master'
Fix Restart On System Theme Change

See merge request videostreaming/grayjay!97
2025-05-09 15:17:52 +00:00
Kai e7e67b9572 organize imports
Changelog: changed
2025-05-09 10:17:15 -05:00
Kai 1a58b693c1 Merge branch 'master' into theme-change-restart
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
2025-05-09 10:14:33 -05:00
Kai 50ecb909b4 Remove cookie
Changelog: changed
2025-05-09 10:08:24 -05:00
Koen 5e480be8db Merge branch 'fullscreen-glitch-fix' into 'master'
Fullscreen UI Glitch Fix

See merge request videostreaming/grayjay!101
2025-05-09 06:43:40 +00:00
Koen J 48a67e51a6 Various fixes. 2025-05-07 20:57:55 +02:00
Koen J 5052bad824 Updated submodules. 2025-05-07 15:46:07 +02:00
Koen J 5be92052bb Fixed device vanishing while still being there in the case the device is blacklisted from starting a relay. 2025-05-07 15:43:28 +02:00
Koen e20945692e Merge branch 'add-plugin-curiosity-stream' into 'master'
add Curiosity Stream plugin

See merge request videostreaming/grayjay!94
2025-05-07 13:03:35 +00:00
Stefan 191a6e2460 add Curiosity Stream plugin 2025-05-07 13:03:35 +00:00
Koen J c813fb4fad Do not show history entries where it could not retrieve a plugin id. 2025-05-07 14:34:57 +02:00
Koen J bf7001b578 Implemented to background button in pip. 2025-05-07 14:17:29 +02:00
Koen J 18102a2a73 Try a more heavy handed approach to get plugin ids for history changes. 2025-05-07 13:26:31 +02:00
Koen J 780c1dbde1 Reverted platform filters on history page. 2025-05-07 13:18:22 +02:00
Koen J 879aab0d99 Added progress bar to playlist items. 2025-05-07 13:08:13 +02:00
Koen J 6f37bc2f5d Confirming sync now brings you back to the device list. 2025-05-07 12:57:44 +02:00
Koen J fc59b841d6 History now filters out videos of plugins that are not enabled. History now has a list of filters to filter specific plugins. History now shows an icon of which platform a specific history video is on. 2025-05-07 12:49:39 +02:00
Koen J c07fcdd489 Prevent going into picture in picture when clicking add sources. 2025-05-07 11:22:54 +02:00
Koen J a49db10ade Fixed issue 2163. 2025-05-07 11:01:25 +02:00
Koen J 77bae98d77 Private mode visibility no longer overlays pip/minimized video. 2025-05-07 10:57:39 +02:00
Koen J 254df7211c Fixed datetime checking related to playlists on android. 2025-05-07 10:36:32 +02:00
Koen J f9caab48c4 Added insensitivity to base64 formats. 2025-05-07 10:12:12 +02:00
Koen J e0b5e7b808 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 16:55:51 +02:00
Koen J ac3a8da002 Various fixes for android to android pairing. 2025-05-06 16:54:58 +02:00
Kelvin 1aa45c2156 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 13:23:10 +02:00
Kelvin 3cf8abd409 Fix racecondition watchlater adds 2025-05-06 13:23:00 +02:00
Koen J db8426779c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 13:04:53 +02:00
Koen J b419e033f3 Casting device more clearly communicates when not ready. Implemented backoffs for SyncServer. 2025-05-06 13:04:42 +02:00
Kelvin d686fa327b Incorrect gzip compression 2025-05-06 12:40:24 +02:00
Kelvin a1ce5eda43 Fix synced ImageVariables showing black images 2025-05-06 11:53:30 +02:00
Koen J 1e790d1aa9 Added toggle to be able to disable local functionality for sync. Sync now automatically closes when pairing is successful. Pairing in progress layouts now properly show again. 2025-05-05 13:34:52 +02:00
Koen J d1d304b758 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-05 12:00:15 +02:00
Koen J e12b500144 Sync disabled by default. 2025-05-05 12:00:04 +02:00
Kelvin bd77651a1e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-05 11:18:57 +02:00
Kelvin 35dc186395 Login edgecase fix 2025-05-05 11:18:46 +02:00
Koen J 07e78e0d12 Fixed sending packets without data in sync protocol. 2025-05-05 10:41:21 +02:00
Koen J 5b8905c1d2 Made ipv6 casting URL fix. 2025-05-04 00:50:12 +02:00
Koen J 158a27cbae Casting fixes. 2025-05-03 21:20:19 +02:00
Koen J 5769b39d78 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-03 21:05:23 +02:00
Koen J 5c96262c75 Crashfix. 2025-05-03 21:05:12 +02:00
Koen J 766f57dc9d Crashfix. 2025-05-03 21:04:39 +02:00
Kelvin 9986078582 Fix sync crash and responsiveness for subs sync 2025-05-03 19:30:47 +02:00
Koen J e047ab5684 Crashfixes. 2025-05-03 17:35:17 +02:00
Koen J a100785ad7 Gzip only for data packets. 2025-05-03 17:09:34 +02:00
Koen J 156eb4d15e Implemented sync protocol gzip. 2025-05-03 15:00:17 +02:00
Koen J dabcfd965f Fixed send on wrong thread. 2025-05-03 12:11:08 +02:00
Koen J d44a71f3be Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-03 11:22:06 +02:00
Koen J f8edd6cf3d Possibel performance improvements to sync under high lat conditions. 2025-05-03 11:21:57 +02:00
Kelvin 2baf53c5a4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-02 12:10:58 +02:00
Kelvin c26e9c281f Easier search type switching on results page, fix search result click channels 2025-05-02 12:10:47 +02:00
Koen J 9f78e9b7dd Crashfix in nsdmanager. StateSync reconnects less often. Channels are closed when sending fails in sync. 2025-05-01 21:53:48 +02:00
Kelvin fdaf41b605 BuildPlatform property 2025-05-01 16:31:51 +02:00
Koen J 89526efe7a Updated submodules. 2025-05-01 14:11:22 +02:00
Koen J 5e3a25c18f Added dialog with loader before QR code scanner shows. 2025-05-01 11:00:33 +02:00
Koen J cf11c4283e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-01 09:56:25 +02:00
Koen J 2dde04b979 Reduced padding on content types search. 2025-05-01 09:55:33 +02:00
Kelvin 8384f227be Plugin refs 2025-04-30 20:02:09 +02:00
Kelvin 697b3bc5f5 SLD domain checking fix, download notification if on metered, check for unstarted downloads on opening ui, minor fixes/imrpovements 2025-04-30 20:00:48 +02:00
Koen J 9e2041521e Made the disconnect button easier to click on casting connected dialog. 2025-04-29 15:27:24 +02:00
Koen J ee7b89ec6e Added new casting dialog. 2025-04-29 15:22:06 +02:00
Koen J 5b143bdc76 Switch to NsdManager. 2025-04-29 08:39:05 +02:00
Kai e3800426c9 Fix https://github.com/futo-org/grayjay-android/issues/1721
By setting video information section to visibility gone when going fullscreen

Changelog: changed
2025-04-28 10:13:26 -05:00
Koen J d9d00e452e Explicitly set network interface in joinGroup. 2025-04-28 16:59:11 +02:00
Koen J 14500e281c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-04-28 13:59:59 +02:00
Koen J c4623c80ff Implemented app id and updated unit tests. 2025-04-28 13:59:50 +02:00
Kelvin 9e17dce9a9 Fix edgecase where activity killed before 5s after opening 2025-04-25 17:40:45 +02:00
Kai 4acc867634 make channel shorts tab work correctly for linked channels
Changelog: changed
2025-04-23 11:38:09 -05:00
Kai 1a061268de restore brightness when leaving app while a video is full screen
Changelog: changed
2025-04-23 09:30:07 -05:00
Koen J daa91986ef Add search type selector to suggestions fragment. 2025-04-22 13:40:05 +02:00
Koen J 63761cfc9a Simplified all searches to use ContentSearchResultsFragment. 2025-04-22 13:08:23 +02:00
Kai 5091a5485a grayjay only supports dark theme. set that on launch
Changelog: changed
2025-04-21 12:37:12 -05:00
Koen J d10026acd1 Added ping loop. 2025-04-21 13:32:58 +02:00
Kai f8f1cababe increase analyze duration
Changelog: changed
2025-04-17 15:06:30 -05:00
Chris Olin ad46841397 Update AndroidManifest.xml
resolves https://github.com/futo-org/grayjay-android/issues/47
2025-04-16 11:17:39 -04:00
Tim B 20fb1e0fd0 Added monochrome launcher icon 2025-04-16 02:44:40 +02:00
Koen J 9347351c37 Fixed issue where it would continuously try to connect over relay. 2025-04-15 09:39:35 +02:00
Kai 42dd8d6152 add TODO
Changelog: changed
2025-04-14 12:40:47 -05:00
Koen J 0ef1f2d40f Added LinkType to Channel. 2025-04-14 15:19:16 +02:00
Koen J b460f9915d Added settings for enabling/disabling remote sync features. Fixed device pairing success showing too early. 2025-04-14 14:41:47 +02:00
Koen J 4e195dfbc3 Rename to direct and relayed. 2025-04-14 10:38:42 +02:00
buzzcola3 38b9fe3017 Fix prevQueueItem always returning the item from _queue, even when _queueShuffled is active 2025-04-13 20:36:15 +02:00
Kai 0a839b4814 fix crash
Changelog: changed
2025-04-11 10:43:33 -05:00
Koen 3c7f7bfca7 Merge branch 'remote-sync' into 'master'
Implemented remote sync.

See merge request videostreaming/grayjay!93
2025-04-11 14:31:47 +00:00
Koen 05230971b3 Implemented remote sync. 2025-04-11 14:31:47 +00:00
Kai 586db317dd catch exception remove UI lag
Changelog: changed
2025-04-10 10:17:10 -05:00
Kai ae36a24ad1 catch exception remove UI lag
Changelog: changed
2025-04-10 10:16:30 -05:00
Kelvin K dccdf72c73 Message change 2025-04-09 23:35:44 +02:00
Kelvin K ca15983a72 Casting message, caching creator images 2025-04-09 23:26:35 +02:00
Kelvin K 4b6a2c9829 Keyboard hide on search end 2025-04-09 21:02:19 +02:00
Kai 9a435f8859 fix loading bar and app switching
Changelog: changed
2025-04-09 12:16:07 -05:00
Kai 81162c5df2 fix crash on early loading
Changelog: changed
2025-04-08 18:58:56 -05:00
Kai c7c3ddfc96 fix progress bar offset
Changelog: changed
2025-04-08 18:43:40 -05:00
Kai 830d3a9022 Merge branch 'master' into shorts-tab 2025-04-08 17:58:55 -05:00
Kai a1c2d19daf refactor shorts code
Changelog: changed
2025-04-08 17:58:31 -05:00
Kelvin K 1755d03a6b Fcast clearer connection/reconnection overlay, disable ipv6 by default 2025-04-09 00:56:49 +02:00
Kelvin K 869b1fc15e Fix pager for landscape 2025-04-08 00:34:52 +02:00
Kelvin K ce2a2f8582 submods 2025-04-07 23:32:57 +02:00
Kelvin K 7b355139fb Subscription persistence fixes, home toggle fixes, subs exchange gzip, etc 2025-04-07 23:31:00 +02:00
Kelvin K b14518edb1 Home filter fixes, persistent sorts, subs exchange fixes, playlist video options 2025-04-05 01:02:50 +02:00
Kelvin K 7d64003d1c Feed filter loading improved, home filters support, various peripheral stuff 2025-04-04 00:37:26 +02:00
Kelvin K 0a59e04f19 Fix ui offset issue when opening video through search url 2025-04-02 23:40:37 +02:00
Kelvin b57abb646f Merge branch 'subs-exchange' into 'master'
Experimental Subs Exchange

See merge request videostreaming/grayjay!91
2025-04-02 21:12:07 +00:00
Kelvin K dd6bde97a9 Playlists sort and search support, Playlist search support, wip local playback, other fixes 2025-04-02 22:53:54 +02:00
quonverbat 004e4be4d3 Added Turkish Translations 2025-04-02 00:32:10 +03:00
Kai bd87a47551 finished UI and interactions
Changelog: added
2025-04-01 11:25:07 -05:00
Kelvin K b545545712 Remove dep 2025-04-01 01:01:45 +02:00
Kelvin K c1993ffa03 SubsExchange fixes 2025-04-01 00:56:24 +02:00
Kelvin K 7f7ebafa46 Resume on playback error instead of reseting, dont error on empty author url, subs exchange fixes 2025-03-31 20:02:49 +02:00
Kelvin K b652597924 chapters ui on text press 2025-03-28 21:40:17 +01:00
Koen 258fe77928 Merge branch 'add-plugin-tedtalks' into 'master'
add ted talks plugin

See merge request videostreaming/grayjay!90
2025-03-28 19:42:30 +00:00
Stefan 5a9fcd6fab add ted talks plugin 2025-03-28 19:13:55 +00:00
Kelvin K 3c05521a5b Chapter Overlay 2025-03-27 23:25:13 +01:00
Kelvin K 034b8b15ae WIP SubsExchange 2025-03-26 23:28:32 +01:00
Kelvin K 7bd687331b AddSource by url support, submods 2025-03-24 20:33:17 +01:00
Kai DeLorenzo bdae35b1a8 Merge branch 'master' into 'small-hls-fixes'
# Conflicts:
#   app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
2025-03-24 02:12:27 +00:00
Kelvin K 54d58df4b6 Sync watch later on initial connection, Original audio boolean support, priority audio support, setting to prefer original audio 2025-03-21 02:23:55 +01:00
Kai DeLorenzo 76103a2a8c fix dialog missing 2025-03-19 15:12:48 -05:00
Kai DeLorenzo 9165a9f7cb Add : support for login button selector 2025-03-17 23:24:15 +00:00
Kelvin b556d1e81d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-03-10 22:12:27 +01:00
Kelvin 7c25678211 Subgroup sub image url, ImageVariable default error icon on fail to load 2025-03-10 22:12:17 +01:00
Kai f63f9dd6db initial POC shorts tab
Changelog: added
2025-03-07 14:27:18 -06:00
Koen J c83a9924e2 Implemented new ApiMethods calls. 2025-03-05 17:04:48 +01:00
Koen J bbeb9b83a0 Removed dynamic Polycentric calls. 2025-03-05 11:58:09 +01:00
Kelvin 06478f3e36 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-27 14:34:59 +01:00
Kelvin 40f20002b2 submods 2025-02-27 14:34:51 +01:00
Koen J 442272f517 SettingsActivity can now be landscape. 2025-02-27 10:38:03 +01:00
Kelvin 88dae8e9c4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-26 21:29:40 +01:00
Kelvin 1bbfa7d39e WIP home filtering 2025-02-26 21:29:06 +01:00
Koen J edc2b3d295 Fixed issue where video reload would reset video timestamp. 2025-02-25 15:32:30 +01:00
Koen J 0006da7385 Implemented sync display names. 2025-02-25 11:00:54 +01:00
Kai 470b7bd2e5 add non iv version
Changelog: added
2025-02-20 21:27:44 -06:00
Kai 9014fb581d add support for downloading encrypted HLS streams
Changelog: changed
2025-02-20 16:07:35 -06:00
Kai DeLorenzo b5ac8b3ec6 Edit Authentication.md 2025-02-20 21:59:29 +00:00
Kai 78f5169880 add recommendations assignment in video details class
Changelog: added
2025-02-20 14:43:13 -06:00
Kai 7ffa6b1bb3 revert params
Changelog: changed
2025-02-19 12:32:17 -06:00
Kai 3cd4b4503f fix other cookie handling
Changelog: changed
2025-02-19 11:59:59 -06:00
Kelvin 3361b77aec Remove accidental always update 2025-02-13 21:00:02 +01:00
Kelvin 8b7c9df286 Add to queue button on recommendations, no toast on add to watch later if dup 2025-02-12 20:16:50 +01:00
Kelvin 157d5b4c36 Fix container id conflict 2025-02-12 20:03:33 +01:00
Kelvin 44c8800bec plugin disabled update check fix 2025-02-12 19:25:29 +01:00
Kelvin 2f0ba1b1f7 Setting to check disabled plugins for updates (off by default) 2025-02-12 19:17:20 +01:00
Kelvin 36c51f1a0c Refs 2025-02-12 19:06:43 +01:00
Kelvin 1dfe18aa6f Add Apple podcasts 2025-02-12 18:58:01 +01:00
Kelvin b9bbfb44c5 Update submodules, fix apple podcast dir 2025-02-12 18:53:30 +01:00
Kelvin 83843f192d Show total downloaded content duration, Indicator how many subscriptions, save queue as playlist 2025-02-12 18:43:15 +01:00
Kelvin 8839d9f1c6 Fix for misisng exports for export playlist 2025-02-12 16:31:30 +01:00
Kelvin 0630ec1d46 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-11 20:31:33 +01:00
Kelvin 4dce8d6a80 Export playlist support 2025-02-11 20:31:26 +01:00
Koen J 3b62f999bf Fixed HttpFileHandler bug causing casting local webm not to work. 2025-02-11 17:41:25 +01:00
Kai d63fa521a1 Merge remote-tracking branch 'origin/hls-audio-fixes' into hls-audio-fixes
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
2025-02-11 10:36:56 -06:00
Kai ca781dfe15 restore hard code HLS as mp4
Changelog: changed
2025-02-11 10:35:05 -06:00
Kai 4bc561ceab fix HLS audio download and download audio only
Changelog: changed
2025-02-11 10:34:39 -06:00
Kai 3d258180bd restore hard code HLS as mp4
Changelog: changed
2025-02-11 10:31:47 -06:00
Kelvin 65ae8610fd Hide download for live videos 2025-02-11 17:06:57 +01:00
Kelvin c1c2000c98 Download container fixes 2025-02-11 16:13:07 +01:00
Kelvin 287c2d82a1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-11 14:13:55 +01:00
Kelvin 5cde1650f4 DashRaw streammetadata fetching 2025-02-11 14:13:48 +01:00
Kelvin a4b90f14ab Merge branch 'hls-audio-only-download' into 'master'
Hide audio only option when no audio sources

See merge request videostreaming/grayjay!87
2025-02-11 12:30:53 +00:00
Koen J 4826b40136 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-11 10:32:24 +01:00
Koen J 62618224da Casting server did not bind to an automatically selected port. 2025-02-11 10:32:11 +01:00
Kai 49f15e1637 don't show audio only download option if there aren't any audio sources available
for HLS and DASH the HLS and DASH pickers give the option to only download audio

Changelog: changed
2025-02-10 22:32:56 -06:00
Kai d5cab0910e fix HLS audio download and download audio only
Changelog: changed
2025-02-10 22:21:06 -06:00
Kai d4ccf232c1 fix HLS download odysee nebula peertube
Changelog: changed
2025-02-10 17:57:27 -06:00
Kelvin e36047c890 Merge branch 'prevent-exception-replay' into 'master'
Prevent Exception Replay When Unsubscribing From Deleted Channel

See merge request videostreaming/grayjay!77
2025-02-10 19:15:09 +00:00
Kelvin 8f1199bd08 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-10 20:12:50 +01:00
Kelvin d6e045ea4e JSDOM, optional packages, Channel not crash if opened without plugin, downloads ordering fixes/naming 2025-02-10 20:12:43 +01:00
Kelvin 304e48996b Merge branch 'rotation-lock-fix' into 'master'
Fix Rotation Lock Activity Resume Issue

See merge request videostreaming/grayjay!83
2025-02-10 18:55:19 +00:00
Kelvin f350dc83b8 Merge branch 'brightness-fix' into 'master'
Restore Overlay Brightness When Re-entering Full Screen

See merge request videostreaming/grayjay!84
2025-02-10 18:53:38 +00:00
Kelvin ebb7beda8c Merge branch 'fix-background-playback' into 'master'
Background playback support for HLS and DASH

See merge request videostreaming/grayjay!80
2025-02-10 18:48:05 +00:00
Kelvin a01f3da66e Merge branch 'adaptive-streaming-auto-ui' into 'master'
Auto mode UI for adaptive streams (HLS and DASH)

See merge request videostreaming/grayjay!76
2025-02-10 18:47:50 +00:00
Kelvin 72f5b5fbc0 Ref 2025-02-07 19:08:38 +01:00
Kelvin 330aa495c8 Playlist dup prevention, download search and ordering, optional package support 2025-02-06 21:36:33 +01:00
Kelvin 0b529ae94d Plugin changelog support, Hide hidden from search setting, No author change warning if missing pubkey, toast on add to playlist, better autoplay description, Playlist total duration label 2025-02-06 19:19:29 +01:00
Kelvin 83b35183d0 Channel shorts tab, Forced batch parallelization, Playlist download support for live sources, hardware codec query 2025-02-05 19:40:28 +01:00
Alexandre Picavet daf1d42a0f feat(player): Add a setting to adjust player seek duration
Create a seekOffset dropdown setting defaulting to 10 seconds.
Update the fastForwardTick method  of the GestureControlView to take the
seekOffset setting into account and update the view accordingly.
2025-02-04 22:00:08 +01:00
Kelvin 2cd01eb1fe Merge 2025-02-03 21:38:18 +01:00
Kelvin 07378f665a Fix http memory leak for byte responses, refs 2025-02-03 21:36:41 +01:00
Kai DeLorenzo bfd5f24f4c Fix https://github.com/futo-org/grayjay-android/issues/727 2025-01-27 21:51:17 +00:00
Kai 3d617187af update rotation lock approach
Changelog: changed
2025-01-27 12:03:25 -06:00
Koen J d040b93ca9 Updated submodules and fixed IPv6 casting play address being wrong. 2025-01-23 14:26:08 +01:00
Kai a1d460385d prevent the user from needing to tap update on system dialog when self updating
Changelog: added
2025-01-20 15:38:26 -06:00
Koen J a410e2962a Only take one line for signing. 2025-01-20 14:04:55 +01:00
Koen J f5aa8f37bb Updated youtube. 2025-01-17 22:19:02 +01:00
Koen J 7e932df450 Updated submodules. 2025-01-17 22:01:26 +01:00
Koen J 3d4741727e Updated submodules. 2025-01-17 21:37:28 +01:00
Kelvin a03b63ef74 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-01-17 21:27:23 +01:00
Kelvin 15ce3e9f20 Timeout support 2025-01-17 21:27:12 +01:00
ajp-dev d2ed0c65ca fix link typo for Script Signing
typo causes HTTP 400 error when accessing link to Script Signing.md
2025-01-17 17:05:49 +08:00
Kai da58b72f9d add background playback support for videos without an explicit audio source
Changelog: changed
2025-01-14 11:36:37 -06:00
Kai DeLorenzo 1639bd7af1 Merge branch 'improve-full-screen-portrait-docs' into 'master'
improve full screen portrait docs

See merge request videostreaming/grayjay!79
2025-01-14 16:00:51 +00:00
Kai d474121f85 improve full screen portrait docs
Changelog: changed
2025-01-14 09:59:17 -06:00
Kai 978f76ffb6 Added current quality to auto item
Changelog: added
2025-01-10 14:38:54 -06:00
Kai 084bac00f5 Clear feed loading exceptions to prevent replay of exceptions
Changelog: changed
2025-01-08 21:54:03 -06:00
Kai 94454172dd Add UI to show when adaptive streams (HLS and DASH) are in auto mode
Changelog: added
2025-01-08 20:43:38 -06:00
Koen J 891d3cf966 Added debug message. 2025-01-06 16:55:02 +01:00
Koen J 561d5ec7ab Fixes to sync. 2025-01-06 15:56:31 +01:00
Koen J 7ce437d50a Updated submodule. 2025-01-06 11:42:14 +01:00
Kelvin 4b02d4ce90 Merge branch 'zvonimir-dev' into 'master'
workflow: Remove labeler

See merge request videostreaming/grayjay!74
2024-12-23 20:44:25 +00:00
Zvonimir Zrakić 3107185869 workflow: Remove labeler 2024-12-23 21:31:06 +01:00
Koen 2e3584a353 Merge branch 'zvonimir-dev' into 'master'
Improve issue templates

See merge request videostreaming/grayjay!73
2024-12-23 18:36:32 +00:00
Zvonimir Zrakić e5b1be195c fix: Fix issue templates, add new plugins 2024-12-23 18:45:35 +01:00
Zvonimir Zrakić dde30c9d76 Add VPN option and fix label typo 2024-12-23 18:45:18 +01:00
Koen 3830e65de8 Update bug_report.yml 2024-12-23 15:43:40 +00:00
Koen c589cf167e Merge branch 'zvonimir-dev' into 'master'
Update issue templates

See merge request videostreaming/grayjay!72
2024-12-21 22:05:42 +00:00
Zvonimir Zrakić 2fde367c82 Update issue templates 2024-12-21 22:05:42 +00:00
Kai DeLorenzo 8fd188268e Merge branch 'disable-download' into 'master'
disable download for Widevine sources

See merge request videostreaming/grayjay!70
2024-12-21 17:27:15 +00:00
Kai b65257df42 disable download for Widevine sources 2024-12-21 11:26:10 -06:00
Kelvin aaa2d7f08d Prevent crashes on non-existing assets 2024-12-19 22:00:56 +01:00
Kelvin f73e25ece6 Fix crash activity update support 2024-12-19 21:54:32 +01:00
Kelvin 78d427f208 Remove broken ref for now 2024-12-19 21:14:07 +01:00
Kelvin eaeaf3538f Better messaging on failed to connect sync 2024-12-18 22:20:11 +01:00
Kai DeLorenzo 85e381a85e Merge branch 'update-deps' into 'master'
update deps

See merge request videostreaming/grayjay!69
2024-12-18 20:37:07 +00:00
Kai 1b7ee8231b update deps 2024-12-18 14:36:40 -06:00
Kai DeLorenzo 1b8b8f5738 Merge branch 'tablet-rotation-issue' into 'master'
more recent landscape and rotation issues

See merge request videostreaming/grayjay!66
2024-12-14 20:42:11 +00:00
Kai 53df19b477 fixes for:
weird tablet issues on some screen sizes
horizontal maximized player on phones
always allow full screen rotation not working
2024-12-14 14:41:28 -06:00
Kai DeLorenzo ccf21b7580 Merge branch 'rotation-regression' into 'master'
fix rotation regression

See merge request videostreaming/grayjay!65
2024-12-14 03:13:21 +00:00
Kai 4189d62a57 fix rotation regression 2024-12-13 20:47:21 -06:00
Koen 9a3e3af614 Merge branch 'feat/apple-podcasts-plugin' into 'master'
Add Apple Podcasts plugin

See merge request videostreaming/grayjay!63
2024-12-13 18:55:16 +00:00
Stefan f7187400dc Add Apple Podcasts plugin 2024-12-13 17:59:56 +00:00
Kai DeLorenzo f55a7f0a7b Merge branch 'detect-system-setting-changes' into 'master'
detect system auto rotate setting changes

See merge request videostreaming/grayjay!62
2024-12-13 17:46:00 +00:00
Kai d6d35a645e detect system auto rotate setting changes
correctly handle lock button when full screen
2024-12-13 11:44:57 -06:00
Kai e719dcc7f5 detect system auto rotate setting changes
correctly handle lock button when full screen
2024-12-13 11:23:40 -06:00
Kai DeLorenzo bc5bc5450c Merge branch 'change-default-auto-rotate-setting' into 'master'
change default to force auto rotate while full screen

See merge request videostreaming/grayjay!61
2024-12-13 16:22:00 +00:00
Kai f4bade0c2e change default to force auto rotate while full screen 2024-12-13 10:21:40 -06:00
Koen J 9be59c674d Updated YouTube. 2024-12-13 17:13:42 +01:00
Kai DeLorenzo a1dec23c20 Merge branch 'default-reset-auto-rotate-disabled' into 'master'
remove assumptions about rotation preference

See merge request videostreaming/grayjay!60
2024-12-13 16:01:07 +00:00
Kai ed926c4e37 remove assumptions about rotation preference 2024-12-13 10:00:43 -06:00
Kai DeLorenzo ab360ed6f6 Merge branch 'force-leave-landscape-tweak' into 'master'
Force leave landscape tweak

See merge request videostreaming/grayjay!59
2024-12-13 15:43:33 +00:00
Kai 569ba3d651 suppress warning 2024-12-13 09:43:08 -06:00
Kai 60fe28c2fe simplified rotation logic 2024-12-13 09:41:52 -06:00
Kai DeLorenzo 2787e29a07 Merge branch 'delay-tweak' into 'master'
increase delay to prevent erroneous rotations

See merge request videostreaming/grayjay!58
2024-12-13 05:02:23 +00:00
Kai c77a4d08d6 increase delay to prevent erroneous rotations 2024-12-12 23:01:28 -06:00
Kai DeLorenzo 9b3f90f922 Merge branch 'auto-rotate-fixes' into 'master'
more rotation/orientation fixes

See merge request videostreaming/grayjay!57
2024-12-12 21:27:20 +00:00
Kai c88d457021 fix device getting stuck landscape or in portrait when entering and exiting full screen via the button
fix weird rotation behavior when locking and unlocking rotation via the player button
2024-12-12 15:25:02 -06:00
Koen J b20b625820 Fixed compiler errors. 2024-12-12 16:02:37 +01:00
Koen J fd95311920 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-12 14:13:34 +01:00
Koen J 6da5c11731 Fixed concurrent modification crash in ServiceRecordAggregator and link clicking in scroll. 2024-12-12 14:13:24 +01:00
Koen 4e58231308 Merge branch 'revamp-rotation-settings' into 'master'
Update and simplify rotation settings

See merge request videostreaming/grayjay!56
2024-12-12 12:56:21 +00:00
Kai ef0ecf249a update rotation settings 2024-12-11 14:38:58 -06:00
Kelvin 4981617f7a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-11 20:24:17 +01:00
Kelvin 2070bc7007 Refs 2024-12-11 20:24:09 +01:00
Kai DeLorenzo 231d2461b3 Merge branch 'incorrect-number-of-columns-bug' into 'master'
fix the calculation that incorrectly sets the number of columns to display

See merge request videostreaming/grayjay!54
2024-12-11 17:48:17 +00:00
Kai DeLorenzo 3b457f87c4 Merge branch 'fix-fullscreen-from-pip' into 'master'
prevent going into full screen when entering pip mode

See merge request videostreaming/grayjay!55
2024-12-11 17:48:00 +00:00
Koen J de3ced4d3c Intent class should be MediaButtonReceiver. 2024-12-11 10:41:36 +01:00
Koen J 891777e89e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-11 10:36:13 +01:00
Koen J 287239dd1c Added media button receiver. 2024-12-11 10:36:02 +01:00
Kelvin 7cdded8fd7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-10 22:35:46 +01:00
Kelvin 8c9d045e1d Download playlist fix for videos without audio file 2024-12-10 22:35:38 +01:00
Kai 620f5a0459 prevent going into full screen when entering pip mode 2024-12-10 11:30:48 -06:00
Koen 178d874ba0 Merge branch 'spinner-block-player' into 'master'
Spinner block player go full screen

See merge request videostreaming/grayjay!53
2024-12-10 16:18:55 +00:00
Koen d44f30c8a6 Merge branch 'creator-filter-clear' into 'master'
enable creator filter clear

See merge request videostreaming/grayjay!52
2024-12-10 15:56:42 +00:00
Koen J ce66937429 Offline playback toast now doesn't show more than once every 5 seconds. 2024-12-10 14:01:34 +01:00
Koen J 9823337375 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-10 13:54:29 +01:00
Koen J 11f5f0dfe1 Fixed comment character counter. 2024-12-10 13:54:02 +01:00
Koen J e1882f19e8 Comment close now requires confirmation. Fixed comment character counter. 2024-12-10 13:52:24 +01:00
Koen J 6a8b9f06c2 Comment close now requires confirmation. 2024-12-10 13:52:02 +01:00
Koen J 752fc8787d Fixed link scrolling behaviour. 2024-12-10 13:29:09 +01:00
Koen J 90a1cd8280 Placing reply comments works again. 2024-12-10 13:07:03 +01:00
Koen J aa570ac29d Updated submodules. 2024-12-10 12:47:18 +01:00
Kai fb7b6363f9 added multi column support for channels 2024-12-09 18:00:24 -06:00
Kai 23afe7994c fix the calculation that incorrectly sets the number of columns to display 2024-12-08 16:47:13 -06:00
Kai 7557e6f6ba prevent going full screen before the video has loaded 2024-12-07 11:32:28 -06:00
Kai 86b6938911 added video detail check 2024-12-07 11:09:48 -06:00
Kai 8f30a45fa8 enable creator filter clear 2024-12-06 11:13:29 -06:00
Koen J 7c9e9d5f52 Should not crash app when StateSync fails to bind. 2024-12-06 17:49:18 +01:00
Koen J 4066ce73a8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-06 14:55:10 +01:00
Koen J b5722dba1a MainActivity should be singleInstance. 2024-12-06 14:54:57 +01:00
Koen 81765ecafc Update deploy-playstore.sh 2024-12-06 10:54:36 +00:00
Kelvin 84b42e9d19 refs 2024-12-05 17:08:30 +01:00
Koen J ed319a0e5f Fixed invalid padding. 2024-12-05 17:03:45 +01:00
Koen J dd55d10194 Crashfix. 2024-12-05 17:00:23 +01:00
Koen J 2084b46090 Possible fix for sub bar distance. 2024-12-05 16:58:35 +01:00
Koen J 53443a6cf2 Only show announcements on subscriptions if home is not enabled. Add margin top to subscriptions. 2024-12-05 16:51:56 +01:00
Koen J 92715b5642 Fixed crash due to AnnouncementView. 2024-12-05 16:40:05 +01:00
Kelvin 6166392515 Polycentric disk caching 2024-12-04 18:20:40 +01:00
Kelvin 49d0dead7d Fix playlist wrong id for local conversion, refs 2024-12-04 01:31:53 +01:00
Kelvin 6f004830ff Fix directly opening playlists urls in wrong ui, causing wrong thread 2024-12-02 16:23:30 +01:00
Kelvin e2e5e36bad Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-28 20:12:58 +01:00
Kelvin f267d264d3 Refs 2024-11-28 20:12:50 +01:00
Kelvin be1a77bfd7 Merge branch 'drm-revamp' into 'master'
Drm revamp

See merge request videostreaming/grayjay!51
2024-11-28 19:10:09 +00:00
Kai DeLorenzo 41a980e826 rename licenseExecutor to licenseRequestExecutor 2024-11-28 13:53:01 -05:00
Kai DeLorenzo 09c09f3d64 switch to executor structure 2024-11-28 13:05:53 -05:00
Kelvin 2404399ec5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-28 16:24:10 +01:00
Kelvin b45d4c0557 Support for per-comment lazy loading for Polycentric 2024-11-28 16:24:01 +01:00
Kai DeLorenzo a41b138d3c source.js mapping 2024-11-26 18:54:47 -05:00
Kai DeLorenzo 1e46949dd6 added dash widevine support and made audio url widevine more flexible 2024-11-26 18:52:15 -05:00
Kai DeLorenzo 3ed2c1ba5d Merge branch 'fix-audio-only-playback' into 'master'
Fix audio only playback

See merge request videostreaming/grayjay!50
2024-11-26 21:01:00 +00:00
Kai DeLorenzo 809b99c9c9 fix null pointer exception 2024-11-26 16:00:13 -05:00
Kai DeLorenzo 4d3acdb5fb Merge branch 'landscape-fixes' into 'master'
Fix black bars on top and bottom of videos

See merge request videostreaming/grayjay!49
2024-11-26 19:06:39 +00:00
Kai DeLorenzo ca9e321ef2 lower minimum video player height 2024-11-26 14:00:35 -05:00
Kelvin da27517fcf Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-26 18:47:40 +01:00
Kelvin 192df0a3b8 Prevent dup history broadcasts 2024-11-26 18:47:33 +01:00
Koen J a965003a9d Fix gray line in video player. Thank you @michael-oberpriller 2024-11-26 18:33:25 +01:00
Koen J 9ea26c821f Updated submodules 2024-11-26 18:10:44 +01:00
Kelvin 14b699485a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-25 17:10:00 +01:00
Kelvin 1684edc43f refs, ui dialog alignment 2024-11-25 17:09:50 +01:00
Kai DeLorenzo 580c4418b9 Merge branch 'landscape' into 'master'
Landscape

See merge request videostreaming/grayjay!34
2024-11-25 15:45:04 +00:00
Kai DeLorenzo 4a65fc2358 document empty override 2024-11-25 10:33:05 -05:00
Kai DeLorenzo 71ba131fb3 subscriptions feed announcements fix 2024-11-23 17:55:01 -05:00
Kai DeLorenzo 9693b50719 koen code review fixes 2024-11-23 17:42:23 -05:00
Kelvin 102e2c54bb Working watchlater sync 2024-11-22 21:51:06 +01:00
Kelvin e989590c08 Merge 2024-11-22 20:35:01 +01:00
Kelvin 6cee33b449 WIP Watchlater sync 2024-11-22 20:33:44 +01:00
Koen J f32498a444 Implemented session id. 2024-11-22 19:02:07 +01:00
Kai DeLorenzo c85f71b601 update full screen strings 2024-11-21 11:47:45 -05:00
Koen J 196e55899e Fixed DASH generation with subtitles. 2024-11-21 17:25:18 +01:00
Koen J ebec45076d Merge branch 'landscape' of gitlab.futo.org:videostreaming/grayjay into landscape 2024-11-21 10:47:12 +01:00
Koen J 561d9ae987 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into landscape 2024-11-21 10:46:39 +01:00
Koen 8950bd94cb Merge branch 'ignore-offline-error' into 'master'
hide unable to resolve host exceptions when there are more videos in a playlist

See merge request videostreaming/grayjay!24
2024-11-21 09:43:52 +00:00
Koen f416f197bc Merge branch 'subscription-submission-modal' into 'master'
Subscription submission modal

See merge request videostreaming/grayjay!38
2024-11-21 09:43:25 +00:00
Koen 65afe5a0e6 Merge branch 'm3u8-parse-fix' into 'master'
fixed m3u8 parsing bug that caused Patreon video downloads to crash Grayjay

See merge request videostreaming/grayjay!39
2024-11-21 09:43:14 +00:00
Koen 4b5d347413 Merge branch 'remove-search-limit' into 'master'
allow searches down to 1 character

See merge request videostreaming/grayjay!40
2024-11-21 09:43:02 +00:00
Kai DeLorenzo 4dcc2dd0ca revert style changes 2024-11-19 11:45:54 -05:00
Kai DeLorenzo 2a7a332160 Merge branch 'master' into 'subscription-submission-modal'
# Conflicts:
#   app/src/main/res/values/strings.xml
2024-11-19 16:29:32 +00:00
Kelvin 27ee1eabda Merge branch 'capabilities-fix' into 'master'
fixed incorrect capabilities call

See merge request videostreaming/grayjay!37
2024-11-19 16:03:15 +00:00
Koen 0034665965 Merge branch 'accessability' into 'master'
fix: Remove extra strings and remove single quote

See merge request videostreaming/grayjay!46
2024-11-18 17:28:16 +00:00
Kai DeLorenzo a69692be18 Merge branch 'master' into 'subscription-submission-modal'
# Conflicts:
#   app/src/main/res/values/strings.xml
2024-11-18 15:05:10 +00:00
Kai DeLorenzo dc76152166 Merge branch 'master' into 'landscape'
# Conflicts:
#   app/src/main/res/layout/activity_polycentric_create_profile.xml
2024-11-18 15:03:06 +00:00
zvonimir d7f3ae696c fix: Remove extra strings and remove single quote 2024-11-18 15:28:48 +01:00
Koen 71f5449d34 Merge branch 'accessability' into 'master'
feat: add android:contentDescription. Closes #1181

See merge request videostreaming/grayjay!45
2024-11-18 13:38:08 +00:00
Koen J 0e64fa8d4c Added a try catch to HandlePacket. 2024-11-18 14:28:56 +01:00
Koen J 73b048d4c5 Fixed resume not always working. 2024-11-18 14:20:31 +01:00
zvonimir 1c05b39861 feat: add android:contentDescription. Closes #1181 2024-11-18 12:54:32 +01:00
Koen J 7cfa6c163f Use SINGLE_TOP instead of CLEAR_TOP and do not start a new task for import data. 2024-11-18 12:48:02 +01:00
Koen J 2d4af2e867 Updated submodules. 2024-11-18 12:35:40 +01:00
Koen 1eeaffc442 Merge branch 'sync-add-subopcode' into 'master'
Added subopcode byte.

See merge request videostreaming/grayjay!43
2024-11-18 11:30:03 +00:00
Koen 82125b33ed Merge branch 'zvonimir-dev' into 'master'
docs: Update CONTRIBUTION.md to reflect changes of accepting

See merge request videostreaming/grayjay!42
2024-11-18 11:23:25 +00:00
Zvonimir Zrakić 42cbbc28fd docs: Update CONTRIBUTION.md to reflect changes of accepting 2024-11-18 11:23:25 +00:00
Koen J a7cbb0e93c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into landscape 2024-11-18 12:16:05 +01:00
Koen J fde6148ece Added subopcode byte. 2024-11-18 11:27:55 +01:00
Koen df1661d75a Merge branch 'zvonimir-dev' into 'master'
Update images in README and add building step for updating & fix services starting

See merge request videostreaming/grayjay!41
2024-11-15 13:42:51 +00:00
zvonimir f938f79a35 fix: Services should not crash if already started 2024-11-15 14:39:15 +01:00
zvonimir 333f00235b docs: Fix source image path 2024-11-15 00:06:20 +01:00
zvonimir c06475bfb3 docs: Update images in README and add building step for updating
submodules
2024-11-15 00:03:36 +01:00
Kelvin d1a54d0cf3 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-14 23:15:23 +01:00
Kelvin 349437c06b Refs, History/subgroup import/export, broadcast sub change to synced devices 2024-11-14 23:15:15 +01:00
Kai DeLorenzo 1b03c83c84 allow searches down to 1 character 2024-11-14 09:18:49 -06:00
Koen J bb749aacf1 Merge branch 'playstore-change' of gitlab.futo.org:videostreaming/grayjay 2024-11-14 10:31:51 +01:00
Kelvin 3a41b89e52 Requests 2024-11-13 18:39:01 +01:00
Kelvin 70cbc77381 Remove items from watchlater if 70% watched 2024-11-12 22:02:08 +01:00
Kelvin 3a99f5dfaa Dup declaration activity 2024-11-12 20:33:56 +01:00
Kelvin f24435ecf4 Confirmation on delete group on group tab 2024-11-12 15:58:20 +01:00
Kelvin 4a708e316a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-12 15:03:08 +01:00
Kelvin c2b47c998d Fallback in case isUpdating not cleared 2024-11-12 15:02:59 +01:00
Koen J 534f7b3134 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-12 14:40:17 +01:00
Koen J d5d2692317 - Fixed crash when ServiceDiscoverer is already started.
- Added music.youtube.com to handled URLs in non-playstore version
- Added open.spotify.com to handled URLs in non-playstore version
- Reverse portrait toggle when on now allows reverse portrait and when off will use last orientation rather than portrait.
- Added playlist video deletion confirmation dialog (with a setting to disable)
- Added a dialog showing the uploaded log id with a copy button
2024-11-12 14:40:04 +01:00
Kelvin dc9cc7b00f WIP updating block for subscriptions 2024-11-12 14:37:57 +01:00
Kelvin 965e74c7e2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-12 13:38:16 +01:00
Kelvin 096ba54eb1 Autobackup password check, Filesize estimation int overflow fix, Deleting subscriptions part of subgroup fix, License delete confirm dialog, double check pager closed runtimes 2024-11-12 13:37:54 +01:00
Koen J f4e38f9e50 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-12 11:51:16 +01:00
Koen J c0d9409176 Added e-mail confirmation for payment and fixed number formats. 2024-11-12 11:51:07 +01:00
zvonimir 7d1f565749 docs: test with lowere max-width 2024-11-11 21:06:02 +01:00
zvonimir dfec4ada3b docs: test new format for images 2024-11-11 21:02:47 +01:00
zvonimir cd695cf265 docs: Update README screenshots 2024-11-11 20:35:06 +01:00
Kelvin 47ff2e0c38 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-11-07 16:37:14 +01:00
Kelvin db7c09291f Subscription group sync, playlist sync, send to device support mobile to desktop 2024-11-07 16:36:49 +01:00
Kai 01f10c49ba remove android:exported attribute from unstable xml 2024-11-06 10:08:08 -06:00
Kai 1ff0692a72 force portrait for most activities 2024-11-06 09:48:19 -06:00
Kai 116e6099d5 prevent landscape for QR code scanner
add scrolling to polycentric creation view
2024-11-05 14:58:39 -06:00
Kai 18ccaadc5b stripped down data to only URL and plugin id. removed modal data preview 2024-11-05 13:55:45 -06:00
Kai 8f6eac7ca2 undo some formatting changes 2024-11-05 13:27:22 -06:00
Kai f4610d0df5 fixed m3u8 parsing bug that caused Patreon video downloads to crash Grayjay 2024-11-04 10:40:48 -06:00
Kai bf1a6b7d0a updated URL and catch HTTP errors 2024-10-30 15:07:29 -05:00
Kai b3fd05e62e fixed incorrect capabilities call 2024-10-28 12:44:23 -05:00
Kai f7ce365618 fix incorrect number of columns in creator search 2024-10-23 15:57:44 -05:00
Kai 77a558dbe5 add subscription submission dialog 2024-10-23 15:43:56 -05:00
Kai cc0c400b28 improved vertical video detection and auto full screen for vertical videos 2024-10-22 13:01:02 -05:00
Koen 2bcd59cbfa Update LICENSE.md 2024-10-21 23:20:54 +00:00
Koen 5139acc7f1 Update LICENSE.md 2024-10-21 23:19:15 +00:00
Kai 1564433e02 added detection of vertical video and improved full screen handling 2024-10-21 13:16:20 -05:00
Kai 1339beb7cd change fullscreen to full screen to align with the more common spelling 2024-10-21 09:59:10 -05:00
Kai cd9698ea48 manifest fixes and sensor update 2024-10-21 09:52:45 -05:00
Kai c8f8e4c5eb Merge branch 'refs/heads/master' into landscape 2024-10-21 09:43:03 -05:00
Koen J 0b4ab46563 Added aws s3 cp for releasing artifacts. 2024-10-20 07:46:41 +02:00
Koen J ea1ac86134 Updated submodules and implemented timer to make sync connect requests less frequent upon failure. 2024-10-19 07:37:12 +02:00
Kelvin 790331e798 History sync support 2024-10-01 14:30:33 +02:00
Kelvin f5d9b2ba41 Harbor link, additional description 2024-09-30 20:20:16 +02:00
Kelvin 7f26ac00b1 Polycentric fixes, missing sync files 2024-09-30 18:28:43 +02:00
Kelvin fcbab10434 Sync implementation for subscriptions, tracking subscription removals, add fcast link to cast tutorial description. 2024-09-30 16:39:10 +02:00
Kelvin c4061cc6ac Minor refactor, handle simple sendToDevice packet 2024-09-26 23:26:52 +02:00
Kelvin 12ac4d6b6f :Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-26 19:17:56 +02:00
Kelvin 3d06e62cd4 Minor dev fix 2024-09-26 19:17:51 +02:00
Koen d7d23e1048 Merge branch 'sync-ui' into 'master'
Sync protocol

See merge request videostreaming/grayjay!35
2024-09-26 12:40:47 +00:00
Koen 1fe9b70176 Sync protocol 2024-09-26 12:40:47 +00:00
Kai a9cf8dd71a more sizing fixes 2024-09-12 10:34:46 -05:00
Kai 3299261db3 fixed video sizing 2024-09-12 10:28:06 -05:00
Kai e465ec8278 remove auto fullscreen for tablets 2024-09-12 08:47:00 -05:00
Kai d0e4a0aa1f update settings 2024-09-11 21:45:18 -05:00
Kai 74efec3235 Merge branch 'refs/heads/master' into landscape
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/SimpleOrientationListener.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
2024-09-11 21:37:34 -05:00
Kai 13516087f2 minimized player fixes 2024-09-11 20:24:16 -05:00
Kelvin 0a0c16524a Allow hiding privacy mode and FAQ without breaking existing orderings 2024-09-10 23:10:36 +02:00
Kelvin 9b843a155e Revert "Allow more tabs to be hidden."
This reverts commit 8c4e511883
2024-09-10 20:51:30 +00:00
Kelvin cb085acbff Submods 2024-09-10 21:06:30 +02:00
Kelvin c3d7df166b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 20:40:17 +02:00
Kelvin d312062125 Fix content recommendations on offline videos 2024-09-10 20:40:14 +02:00
Koen J e2453192aa More gracefully handle failing to set plugin auth. 2024-09-10 17:27:00 +02:00
Kai 68eb0cc8f2 added comment 2024-09-10 09:40:24 -05:00
Kai cb9cecfa5d rotation fixes 2024-09-10 09:36:59 -05:00
Koen J 0f4e4a7d97 Allow configuring stability threshold time and ensure there is no more than 1 job active at a time for SimpleOrientationListener. 2024-09-10 15:54:27 +02:00
Koen J f20a708b36 Check both length and null for 'No recommendations found' 2024-09-10 12:25:30 +02:00
Koen J 8c4e511883 Allow more tabs to be hidden. 2024-09-10 12:24:42 +02:00
Koen J a4a3b8d664 Implement full autorotate lock (default off). 2024-09-10 11:59:44 +02:00
Koen J bf6530ea81 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 10:34:46 +02:00
Koen J 4a80c2aab1 Moved Autoplay button to top and load recommendations now appropriately uses 'StatePlatform.instance.getContentRecommendations(v.url)' for local videos. 2024-09-10 10:34:36 +02:00
Kelvin 527bbfe43f Fix watchlater re-downloading every time videos are reordered 2024-09-09 23:07:15 +02:00
Koen J d8e1edb60b Added autoplay icon. 2024-09-09 15:52:55 +02:00
Koen J 245b5f74c0 Increased scrubber size a bit and made add comment view invisible for platform comments. 2024-09-09 15:50:36 +02:00
Koen J e9a1f63415 Added autoplay setting. 2024-09-09 15:20:31 +02:00
Koen J ec370dd94b Added autoplay feature. 2024-09-09 14:58:08 +02:00
Koen J e39d862ef3 Added rotation zone setting allowing you to specify the rotation to be less sensitive (default 45 degrees). Added reverse portrait setting allowing you to allow reverse portrait (default off). Added setting to hide recommendations. 2024-09-09 12:41:16 +02:00
Koen J 7b065654aa Updated submodules. 2024-09-09 10:52:52 +02:00
Kelvin 918b2bbe96 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 18:46:33 +02:00
Kelvin e529a3d34d Temporariyl disable video cache 2024-09-06 18:46:26 +02:00
Koen J 5475778d67 Force reload. 2024-09-06 18:24:52 +02:00
Kai DeLorenzo da6eef905c hide unable to resolve host exceptions when there are more videos in a playlist 2024-07-05 11:48:35 -05:00
670 changed files with 42317 additions and 7861 deletions
+2
View File
@@ -0,0 +1,2 @@
aar/* filter=lfs diff=lfs merge=lfs -text
app/aar/* filter=lfs diff=lfs merge=lfs -text
+166
View File
@@ -0,0 +1,166 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug", "Android"]
title: "Bug: "
type: bug
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: reproduction-steps
attributes:
label: Reproduction steps
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
placeholder: |
0. Play a YouTube video
1. Press on Download button
2. Select quality 1440p
3. Grayjay crashes when attempting to download
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: Actual result
description: What happend?
placeholder: Tell us what you saw!
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: Expected result
description: What was suppose to happen?
placeholder: Tell us what you expected to happen!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "311"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- "All"
- "Apple Podcasts"
- "BiliBili (CN)"
- "Bitchute"
- "Crunchyroll"
- "CuriosityStream"
- "Dailymotion"
- "Kick"
- "Nebula"
- "Odysee"
- "Patreon"
- "PeerTube"
- "Rumble"
- "SoundCloud"
- "Spotify"
- "TedTalks"
- "Twitch"
- "YouTube"
- "Other"
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: input
id: android-version
attributes:
label: Which android version are you using?
placeholder: "Android 15"
validations:
required: true
- type: input
id: phone-model
attributes:
label: Which device are you using?
placeholder: "Google Pixel 9"
validations:
required: true
- type: input
id: os-version
attributes:
label: Which operating system are you using?
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
validations:
required: true
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: dropdown
id: vpn
attributes:
label: Are you using a VPN?
multiple: false
options:
- "No"
- "Yes"
validations:
required: true
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
@@ -1,15 +1,16 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
labels: ["enhancement", "new"]
labels: ["Enhancement", "Android"]
title: "Feature request: "
type: feature
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a feature request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
@@ -55,4 +56,4 @@ body:
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
@@ -1,13 +1,16 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["documentation", "new"]
labels: ["Documentation"]
title: "Documentation: "
type: task
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a documentation change request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
-80
View File
@@ -1,80 +0,0 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["bug", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "242"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- All
- Youtube
- BiliBili (CN)
- Twitch
- Odysee
- Rumble
- Kick
- PeerTube
- Patreon
- Nebula
- SoundCloud
- Other
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
-34
View File
@@ -1,34 +0,0 @@
name: Issue labeler
on:
issues:
types: [ opened ]
permissions:
contents: read
jobs:
label-component:
runs-on: ubuntu-latest
permissions:
# required for all workflows
issues: write
steps:
- uses: actions/checkout@v3
- name: Parse issue form
uses: stefanbuck/github-issue-parser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- name: Set labels based on plugin field
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: plugin
block-list: |
None
Other
token: ${{ secrets.GITHUB_TOKEN }}
+30
View File
@@ -82,3 +82,33 @@
[submodule "app/src/stable/assets/sources/dailymotion"]
path = app/src/stable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
[submodule "app/src/stable/assets/sources/apple-podcast"]
path = app/src/stable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
path = app/src/unstable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/stable/assets/sources/tedtalks"]
path = app/src/stable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/unstable/assets/sources/tedtalks"]
path = app/src/unstable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/stable/assets/sources/curiositystream"]
path = app/src/stable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/curiositystream"]
path = app/src/unstable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/crunchyroll"]
path = app/src/unstable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/crunchyroll"]
path = app/src/stable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/mixcloud"]
path = app/src/stable/assets/sources/mixcloud
url = ../plugins/mixcloud.git
[submodule "app/src/unstable/assets/sources/mixcloud"]
path = app/src/unstable/assets/sources/mixcloud
url = ../plugins/mixcloud.git
+16 -2
View File
@@ -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).
---
+2 -2
View File
@@ -1,4 +1,4 @@
# Grayjay Core License 1.0
# Source First License 1.1
## Acceptance
By using the software, you agree to all of the terms and conditions below.
@@ -16,7 +16,7 @@ Notwithstanding the above, you may not remove or obscure any functionality in th
You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensors trademarks is subject to applicable law.
## Patents
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
If you make any written claim that the software infringes or contributes to infringement of any patent, your license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company.
## Notices
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.
+19 -16
View File
@@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
</tr>
<tr>
<td>Video</td>
@@ -24,12 +24,10 @@ The FUTO media app is a player that exposes multiple video websites as sources i
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td>
</tr>
<tr>
<td>Sources (all enabled)</td>
<td>Sources (one disabled)</td>
<td>Sources</td>
</tr>
</table>
@@ -38,7 +36,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
</tr>
<tr>
<td>Install a new source</td>
@@ -54,8 +52,8 @@ When a user enters a search term into the search bar, the query is posted to th
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
</tr>
<tr>
<td>Search (list)</td>
@@ -71,7 +69,7 @@ Creators are able to configure their profile using NeoPass.
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
</tr>
<tr>
<td>Channel</td>
@@ -112,7 +110,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
</tr>
<tr>
<td>Settings</td>
@@ -125,8 +123,8 @@ Playlists allow you to make a collection of videos that you can create and custo
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
</tr>
<tr>
<td>Playlists</td>
@@ -142,7 +140,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
</tr>
<tr>
<td>Downloads</td>
@@ -157,7 +155,7 @@ For more information about casting please click [here](./docs/casting.md).
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
</tr>
<tr>
<td>Casting</td>
@@ -182,6 +180,12 @@ In the future we hope to offer users the choice of their desired recommendation
1. Download a copy of the repository.
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).
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
size 65512557
+14 -5
View File
@@ -39,7 +39,7 @@ protobuf {
android {
namespace 'com.futo.platformplayer'
compileSdk 34
compileSdk 36
flavorDimensions "buildType"
productFlavors {
stable {
@@ -97,7 +97,7 @@ android {
defaultConfig {
minSdk 28
targetSdk 34
targetSdk 35
versionCode gitVersionCode
versionName gitVersionName
@@ -154,9 +154,10 @@ android {
}
dependencies {
implementation 'com.google.dagger:dagger:2.48'
//implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
implementation 'com.google.android.material:material:1.12.0'
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
implementation 'androidx.core:core-ktx:1.12.0'
@@ -180,6 +181,7 @@ dependencies {
//JS
implementation("com.caoccao.javet:javet-android:3.0.2")
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
//Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1'
@@ -197,7 +199,8 @@ dependencies {
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
implementation fileTree(dir: 'aar', include: ['*.aar'])
implementation 'com.arthenica:smart-exception-java:0.2.1'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1'
@@ -228,4 +231,10 @@ dependencies {
testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//Rust casting SDK
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
// Polycentricandroid includes this
exclude group: 'net.java.dev.jna'
}
}
@@ -0,0 +1,38 @@
package com.futo.platformplayer
import android.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test
import toAndroidColor
class CSSColorTests {
@Test
fun test1() {
val androidHex = "#80336699"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#33669980"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor(),
)
}
@Test
fun test2() {
val androidHex = "#123ABC"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#123ABCFF"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor()
)
}
}
@@ -0,0 +1,338 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
import org.junit.Assert.*
import org.junit.Test
import java.net.Socket
import java.nio.ByteBuffer
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/*
class SyncServerTests {
//private val relayHost = "relay.grayjay.app"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.138"
private val relayPort = 9000
/** Creates a client connected to the live relay server. */
private suspend fun createClient(
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
onException: ((Throwable) -> Unit)? = null
): SyncSocketSession = withContext(Dispatchers.IO) {
val p = Noise.createDH("25519")
p.generateKeyPair()
val socket = Socket(relayHost, relayPort)
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
val tcs = CompletableDeferred<Boolean>()
val socketSession = SyncSocketSession(
relayHost,
p,
inputStream,
outputStream,
onClose = { socket.close() },
onHandshakeComplete = { s ->
onHandshakeComplete?.invoke(s)
tcs.complete(true)
},
onData = onData ?: { _, _, _, _ -> },
onNewChannel = onNewChannel ?: { _, _ -> },
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
)
socketSession.authorizable = AlwaysAuthorized()
try {
socketSession.startAsInitiator(relayKey)
} catch (e: Throwable) {
onException?.invoke(e)
}
withTimeout(5000.milliseconds) { tcs.await() }
return@withContext socketSession
}
@Test
fun multipleClientsHandshake_Success() = runBlocking {
val client1 = createClient()
val client2 = createClient()
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
client1.stop()
client2.stop()
}
@Test
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
delay(100.milliseconds)
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
assertNotNull("Client B should receive connection info", infoB)
assertEquals(12345.toUShort(), infoB!!.port)
assertNull("Client C should not receive connection info (unauthorized)", infoC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun relayedTransport_Bidirectional_Success() = runBlocking {
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
val tcsDataA = CompletableDeferred<ByteArray>()
channelA.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
}
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
assertArrayEquals(maxSizeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
val data = byteArrayOf(1, 2, 3)
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
assertTrue(success)
assertNotNull(recordB)
assertArrayEquals(data, recordB!!.first)
assertNull("Unauthorized client should not access record", recordC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun getNonExistentRecord_ReturnsNull() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
assertNull("Getting non-existent record should return null", record)
clientA.stop()
clientB.stop()
}
@Test
fun updateRecord_TimestampUpdated() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val key = "updateKey"
val data1 = byteArrayOf(1)
val data2 = byteArrayOf(2)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
val record1 = clientB.getRecord(clientA.localPublicKey, key)
delay(1000.milliseconds)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
val record2 = clientB.getRecord(clientA.localPublicKey, key)
assertNotNull(record1)
assertNotNull(record2)
assertTrue(record2!!.second > record1!!.second)
assertArrayEquals(data2, record2.first)
clientA.stop()
clientB.stop()
}
@Test
fun deleteRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val data = byteArrayOf(1, 2, 3)
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
assertTrue(success)
assertNull(record)
clientA.stop()
clientB.stop()
}
@Test
fun listRecordKeys_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val keys = arrayOf("key1", "key2", "key3")
keys.forEach { key ->
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
}
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
clientA.stop()
clientB.stop()
}
@Test
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
assertArrayEquals(largeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetLargeRecord_Success() = runBlocking {
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
val clientA = createClient()
val clientB = createClient()
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
assertTrue(success)
assertNotNull(record)
assertArrayEquals(largeData, record!!.first)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithValidAppId_Success() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
)
val clientA = createClient()
// Act: Start relayed channel with valid appId
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
val channelB = withTimeout(5.seconds) { tcsB.await() }
withTimeout(5.seconds) { channelTask.await() }
// Assert: Channel is established
assertNotNull("Channel should be created on target with valid appId", channelB)
// Clean up
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val invalidAppId = 5678u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
onException = { }
)
val clientA = createClient()
// Act & Assert: Attempt with invalid appId should fail
try {
withTimeout(5.seconds) {
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
}
fail("Starting relayed channel with invalid appId should fail")
} catch (e: Throwable) {
// Expected: The channel creation should time out or fail
}
// Ensure no channel was created on client B
val completedTask = select {
tcsB.onAwait { "channel" }
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
}
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
// Clean up
clientA.stop()
clientB.stop()
}
}
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
}*/
@@ -0,0 +1,512 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
import kotlin.random.Random
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds
/*
data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream,
val responderInput: LittleEndianDataInputStream,
val responderOutput: LittleEndianDataOutputStream
)
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
typealias OnClose = (SyncSocketSession) -> Unit
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
class SyncSocketTests {
private fun createPipeStreams(): PipeStreams {
val initiatorOutput = PipedOutputStream()
val responderOutput = PipedOutputStream()
val responderInput = PipedInputStream(initiatorOutput)
val initiatorInput = PipedInputStream(responderOutput)
return PipeStreams(
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
)
}
fun generateKeyPair(): DHState {
val p = Noise.createDH("25519")
p.generateKeyPair()
return p
}
private fun createSessions(
initiatorInput: LittleEndianDataInputStream,
initiatorOutput: LittleEndianDataOutputStream,
responderInput: LittleEndianDataInputStream,
responderOutput: LittleEndianDataOutputStream,
initiatorKeyPair: DHState,
responderKeyPair: DHState,
onInitiatorHandshakeComplete: OnHandshakeComplete,
onResponderHandshakeComplete: OnHandshakeComplete,
onInitiatorClose: OnClose? = null,
onResponderClose: OnClose? = null,
onClose: OnClose? = null,
isHandshakeAllowed: IsHandshakeAllowed? = null,
onDataA: OnData? = null,
onDataB: OnData? = null
): Pair<SyncSocketSession, SyncSocketSession> {
val initiatorSession = SyncSocketSession(
"", initiatorKeyPair, initiatorInput, initiatorOutput,
onClose = {
onClose?.invoke(it)
onInitiatorClose?.invoke(it)
},
onHandshakeComplete = onInitiatorHandshakeComplete,
onData = onDataA,
isHandshakeAllowed = isHandshakeAllowed
)
val responderSession = SyncSocketSession(
"", responderKeyPair, responderInput, responderOutput,
onClose = {
onClose?.invoke(it)
onResponderClose?.invoke(it)
},
onHandshakeComplete = onResponderHandshakeComplete,
onData = onDataB,
isHandshakeAllowed = isHandshakeAllowed
)
return Pair(initiatorSession, responderSession)
}
@Test
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val invalidPairingCode = "wrong"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
responderSession.startAsResponder()
withTimeout(100.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val pairingCode = "unnecessary"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val smallData = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(smallData, receivedData)
}
@Test
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(maxData, receivedData)
}
@Test
fun stream_LargeData_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(largeData, receivedData)
}
@Test
fun authorizedSession_CanSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize both sessions
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(data, receivedData)
}
@Test
fun unauthorizedSession_CannotSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, _, _, _ -> }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize initiator but not responder
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Unauthorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
delay(1.seconds)
assertFalse(tcsDataReceived.isCompleted)
}
@Test
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
assertNotNull(initiatorSession.remotePublicKey)
assertNotNull(responderSession.remotePublicKey)
}
@Test
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val invalidAppId = 5678u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
}
class Authorized : IAuthorizable {
override val isAuthorized: Boolean = true
}
class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false
}*/
+21 -9
View File
@@ -36,6 +36,12 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service android:name=".services.MediaPlaybackService"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
@@ -49,11 +55,10 @@
<activity
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask"
android:launchMode="singleInstance"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
@@ -146,14 +151,11 @@
<data android:scheme="polycentric" />
</intent-filter>
</activity>
<activity
android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.DeveloperActivity"
@@ -173,7 +175,6 @@
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.AddSourceActivity"
android:screenOrientation="sensorPortrait"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter>
@@ -217,7 +218,6 @@
android:name=".activities.ManageTabsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait"
@@ -226,5 +226,17 @@
android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncHomeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncPairActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application>
</manifest>
</manifest>
+22 -2
View File
@@ -1022,15 +1022,35 @@
return x.value
});
let settingsToUse = __DEV_SETTINGS ?? {};
if (true) {
for (let setting of this.Plugin?.currentPlugin?.settings) {
if (typeof settingsToUse[setting.variable] == "undefined") {
switch (setting?.type?.toLowerCase()) {
case "boolean":
settingsToUse[setting.variable] = setting.default === 'true';
break;
case "dropdown":
let dropDownIndex = parseInt(setting.default);
if (dropDownIndex) {
settingsToUse[setting.variable] = setting.options[dropDownIndex];
}
break;
}
}
}
}
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = __DEV_SETTINGS;
parameterVals[1] = settingsToUse;
else
parameterVals.push(__DEV_SETTINGS);
parameterVals.push(settingsToUse);
}
const func = source[name];
File diff suppressed because one or more lines are too long
+106 -8
View File
@@ -11,7 +11,8 @@ let Type = {
Streams: "STREAMS",
Mixed: "MIXED",
Live: "LIVE",
Subscriptions: "SUBSCRIPTIONS"
Subscriptions: "SUBSCRIPTIONS",
Shorts: "SHORTS"
},
Order: {
Chronological: "CHRONOLOGICAL"
@@ -31,7 +32,8 @@ let Type = {
Text: {
RAW: 0,
HTML: 1,
MARKUP: 2
MARKUP: 2,
CODE: 3
},
Chapter: {
NORMAL: 0,
@@ -65,6 +67,7 @@ class ScriptException extends Error {
super(arguments[0]);
this.plugin_type = "ScriptException";
this.message = arguments[0];
this.msg = arguments[0];
}
else {
super(msg);
@@ -101,6 +104,12 @@ class UnavailableException extends ScriptException {
super("UnavailableException", msg);
}
}
class ReloadRequiredException extends ScriptException {
constructor(msg, reloadData) {
super("ReloadRequiredException", msg);
this.reloadData = reloadData;
}
}
class AgeException extends ScriptException {
constructor(msg) {
super("AgeException", msg);
@@ -243,7 +252,11 @@ class PlatformVideo extends PlatformContent {
this.duration = obj.duration ?? -1; //Long
this.viewCount = obj.viewCount ?? -1; //Long
this.playbackTime = obj.playbackTime ?? -1;
this.playbackDate = obj.playbackDate ?? undefined;
this.isLive = obj.isLive ?? false; //Boolean
this.isShort = !!obj.isShort ?? false;
}
}
class PlatformVideoDetails extends PlatformVideo {
@@ -260,6 +273,11 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
}
}
@@ -284,15 +302,39 @@ class PlatformPostDetails extends PlatformPost {
}
}
class PlatformArticleDetails extends PlatformContent {
class PlatformWeb extends PlatformContent {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWeb";
}
}
class PlatformWebDetails extends PlatformWeb {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWebDetails";
this.html = obj.html;
}
}
class PlatformArticle extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticle";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class PlatformArticleDetails extends PlatformArticle {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class ArticleSegment {
@@ -308,9 +350,17 @@ class ArticleTextSegment extends ArticleSegment {
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
constructor(images, caption) {
super(2);
this.images = images;
this.caption = caption;
}
}
class ArticleHeaderSegment extends ArticleSegment {
constructor(content, level) {
super(3);
this.level = level;
this.content = content;
}
}
class ArticleNestedSegment extends ArticleSegment {
@@ -367,6 +417,16 @@ class VideoUrlSource {
this.requestModifier = obj.requestModifier;
}
}
class VideoUrlWidevineSource extends VideoUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "VideoUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj) {
super(obj);
@@ -399,8 +459,32 @@ class AudioUrlWidevineSource extends AudioUrlSource {
super(obj);
this.plugin_type = "AudioUrlWidevineSource";
this.bearerToken = obj.bearerToken;
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
// deprecated api conversion
if(obj.bearerToken) {
this.getLicenseRequestExecutor = () => {
return {
executeRequest: (url, _headers, _method, license_request_data) => {
const response = http.POST(
url,
license_request_data,
{ Authorization: `Bearer ${obj.bearerToken}` },
false,
true
);
if (!response.body) {
throw new ScriptException("Unable to acquire license key");
}
return response.body;
}
}
}
}
}
}
class AudioUrlRangeSource extends AudioUrlSource {
@@ -443,6 +527,16 @@ class DashSource {
this.requestModifier = obj.requestModifier;
}
}
class DashWidevineSource extends DashSource {
constructor(obj) {
super(obj);
this.plugin_type = "DashWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class DashManifestRawSource {
constructor(obj) {
obj = obj ?? {};
@@ -550,6 +644,8 @@ class PlatformComment {
this.date = obj.date ?? 0;
this.replyCount = obj.replyCount ?? 0;
this.context = obj.context ?? {};
if(obj.getReplies)
this.getReplies = obj.getReplies;
}
}
@@ -621,11 +717,12 @@ class LiveEventViewCount extends LiveEvent {
}
}
class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail) {
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
super(100);
this.targetUrl = targetUrl;
this.targetName = targetName;
this.targetThumbnail = targetThumbnail;
this.isOutgoing = isOutgoing ?? true;
}
}
@@ -698,6 +795,7 @@ let plugin = {
//To override by plugin
const source = {
getHome() { return new ContentPager([], false, {}); },
getShorts() { return new VideoPager([], false, {}); },
enable(config){ },
disable() {},
@@ -1,122 +0,0 @@
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class AdvancedOrientationListener(private val activity: Activity, private val lifecycleScope: CoroutineScope) {
private val sensorManager: SensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
private val magnetometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastOrientationChangeTime = 0L
private val debounceTime = 200L
private val stabilityThresholdTime = 800L
private var deviceAspectRatio: Float = 1.0f
private val gravity = FloatArray(3)
private val geomagnetic = FloatArray(3)
private val rotationMatrix = FloatArray(9)
private val orientationAngles = FloatArray(3)
val onOrientationChanged = Event1<Int>()
private val sensorListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
when (event.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> {
System.arraycopy(event.values, 0, gravity, 0, gravity.size)
}
Sensor.TYPE_MAGNETIC_FIELD -> {
System.arraycopy(event.values, 0, geomagnetic, 0, geomagnetic.size)
}
}
if (gravity.isNotEmpty() && geomagnetic.isNotEmpty()) {
val success = SensorManager.getRotationMatrix(rotationMatrix, null, gravity, geomagnetic)
if (success) {
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
val newOrientation = when {
roll in -155f .. -15f && isWithinThreshold(pitch, 0f, 30.0) -> {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
roll in 15f .. 155f && isWithinThreshold(pitch, 0f, 30.0) -> {
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
}
isWithinThreshold(pitch, -90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
isWithinThreshold(pitch, 90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
}
else -> lastOrientation
}
//Logger.i("AdvancedOrientationListener", "newOrientation = ${newOrientation}, roll = ${roll}, pitch = ${pitch}, azimuth = ${azimuth}")
if (newOrientation != lastStableOrientation) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastOrientationChangeTime > debounceTime) {
lastOrientationChangeTime = currentTime
lastStableOrientation = newOrientation
lifecycleScope.launch(Dispatchers.Main) {
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
}
}
}
}
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
private fun isWithinThreshold(value: Float, target: Float, threshold: Double): Boolean {
return Math.abs(value - target) <= threshold
}
init {
sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
sensorManager.registerListener(sensorListener, magnetometer, SensorManager.SENSOR_DELAY_GAME)
val metrics = activity.resources.displayMetrics
deviceAspectRatio = (metrics.heightPixels.toFloat() / metrics.widthPixels.toFloat())
if (deviceAspectRatio == 0.0f)
deviceAspectRatio = 1.0f
lastOrientation = activity.resources.configuration.orientation
}
fun stopListening() {
sensorManager.unregisterListener(sensorListener)
}
companion object {
private val TAG = "AdvancedOrientationListener"
}
}
@@ -0,0 +1,319 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 0f
max == r -> ((g - b) / d % 6f) * 60f
max == g -> (((b - r) / d) + 2f) * 60f
else -> (((r - g) / d) + 4f) * 60f
}.let { if (it < 0f) it + 360f else it }
_hslDirty = false
}
/**
* Set all three HSL channels at once.
* Hue in degrees [0...360), s/l [0...1].
*/
fun setHsl(h: Float, s: Float, l: Float) {
val hh = ((h % 360f) + 360f) % 360f
val cc = (1f - abs(2f * l - 1f)) * s
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
val m = l - cc / 2f
val (rp, gp, bp) = when {
hh < 60f -> Triple(cc, x, 0f)
hh < 120f -> Triple(x, cc, 0f)
hh < 180f -> Triple(0f, cc, x)
hh < 240f -> Triple(0f, x, cc)
hh < 300f -> Triple(x, 0f, cc)
else -> Triple(cc, 0f, x)
}
r = rp + m; g = gp + m; b = bp + m
_h = hh; _s = s; _l = l; _hslDirty = false
}
/** Return 0xRRGGBBAA int */
fun toRgbaInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
}
/** Return 0xAARRGGBB int */
fun toArgbInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
}
// — Convenience modifiers (chainable) —
/** Lighten by fraction [0...1] */
fun lighten(fraction: Float): CSSColor = apply {
lightness = (lightness + fraction).coerceIn(0f, 1f)
}
/** Darken by fraction [0...1] */
fun darken(fraction: Float): CSSColor = apply {
lightness = (lightness - fraction).coerceIn(0f, 1f)
}
/** Increase saturation by fraction [0...1] */
fun saturate(fraction: Float): CSSColor = apply {
saturation = (saturation + fraction).coerceIn(0f, 1f)
}
/** Decrease saturation by fraction [0...1] */
fun desaturate(fraction: Float): CSSColor = apply {
saturation = (saturation - fraction).coerceIn(0f, 1f)
}
/** Rotate hue by degrees (can be negative) */
fun rotateHue(degrees: Float): CSSColor = apply {
hue = (hue + degrees) % 360f
}
companion object {
/** Create from Android 0xAARRGGBB */
@JvmStatic fun fromArgb(color: Int): CSSColor {
val a = ((color ushr 24) and 0xFF) / 255f
val r = ((color ushr 16) and 0xFF) / 255f
val g = ((color ushr 8) and 0xFF) / 255f
val b = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
/** Create from Android 0xRRGGBBAA */
@JvmStatic fun fromRgba(color: Int): CSSColor {
val r = ((color ushr 24) and 0xFF) / 255f
val g = ((color ushr 16) and 0xFF) / 255f
val b = ((color ushr 8) and 0xFF) / 255f
val a = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
return fromArgb(color)
}
private val NAMED_HEX = mapOf(
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
"yellowgreen" to "9ACD32"
)
private val NAMED: Map<String, Int> = NAMED_HEX
.mapValues { (_, hexRgb) ->
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
val rgb = hexRgb.toInt(16)
(rgb shl 8) or 0xFF
} + ("transparent" to 0x00000000)
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
@JvmStatic
fun parseColor(s: String): CSSColor {
val str = s.trim()
// named
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
// hex
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
return parseHexPart(part)
}
// rgb/rgba
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseRgbParts(it.split(',').map(String::trim))
}
// hsl/hsla
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseHslParts(it.split(',').map(String::trim))
}
error("Cannot parse color: \"$s\"")
}
private fun parseHexPart(p: String): CSSColor {
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
val hex = when (p.length) {
3 -> p.map { "$it$it" }.joinToString("") + "FF"
4 -> p.map { "$it$it" }.joinToString("")
6 -> p + "FF"
8 -> p
else -> error("Invalid hex color: #$p")
}
val parsed = hex.toLong(16).toInt()
val alpha = (parsed and 0xFF) shl 24
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
val argb = alpha or rgbOnly
return fromArgb(argb)
}
private fun parseRgbParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
// r/g/b: "128" → 128/255, "50%" → 0.5
fun channel(ch: String): Float =
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
else ch.toFloat().coerceIn(0f, 255f) / 255f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
else a.toFloat().coerceIn(0f, 1f)
val r = channel(parts[0])
val g = channel(parts[1])
val b = channel(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(r, g, b, a)
}
private fun parseHslParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
fun hueOf(h: String): Float = when {
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
else -> h.toFloat()
}
// for s and l you only ever see percentages
fun pct(p: String): Float =
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) pct(a)
else a.toFloat().coerceIn(0f, 1f)
val h = hueOf(parts[0])
val s = pct(parts[1])
val l = pct(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
}
}
}
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
fun CSSColor.toAndroidColor(): Int = toArgbInt()
@@ -14,7 +14,6 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@@ -226,6 +225,25 @@ fun Long.toHumanTime(isMs: Boolean): String {
else
return "${prefix}${minsStr}:${secsStr}"
}
fun Long.toHumanDuration(isMs: Boolean): String {
var scaler = 1;
if(isMs)
scaler = 1000;
val v = Math.abs(this);
val hours = Math.max(v/(secondsInHour*scaler), 0);
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
val minsStr = mins.toString();
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
val secsStr = seconds.toString().padStart(2, '0');
val prefix = if (this < 0) { "-" } else { "" };
return listOf(
if(hours > 0) "${hours}h" else null,
if(mins > 0) "${mins}m" else null ,
if(seconds > 0) "${seconds}s" else null
).filterNotNull().joinToString(" ");
}
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
fun String.fixHtmlWhitespace(): Spanned {
@@ -357,14 +375,19 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
fun String.matchesDomain(queryDomain: String): Boolean {
if(queryDomain.startsWith(".")) {
val parts = queryDomain.lowercase().split(".");
if(parts.size < 3)
val parts = this.lowercase().split(".");
val queryParts = queryDomain.lowercase().trimStart("."[0]).split(".");
if(queryParts.size < 2)
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
if(parts.size >= 3){
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
if(isSLD && parts.size <= 3)
else {
val possibleDomain = "." + queryParts.joinToString(".");
if(slds.contains(possibleDomain))
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
/*
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
if(isSLD && queryParts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
*/
}
//TODO: Should be safe, but double verify if can't be exploited
@@ -376,9 +399,11 @@ fun String.matchesDomain(queryDomain: String): Boolean {
fun String.getSubdomainWildcardQuery(): String {
val domainParts = this.split(".");
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
if(slds.contains(sldParts))
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
var wildcardDomain = if(domainParts.size > 2)
"." + domainParts.drop(1).joinToString(".")
else
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
"." + domainParts.joinToString(".");
if(slds.contains(wildcardDomain.lowercase()))
"." + domainParts.joinToString(".");
return wildcardDomain;
}
@@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
@@ -215,8 +216,13 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this);
}
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
ensureNotMainThread()
val timeout = 10000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
if (addresses.isEmpty()) {
return null;
@@ -235,8 +241,11 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
return null;
}
val sortedAddresses: List<InetAddress> = addresses
.sortedBy { addr -> addressScore(addr) }
val sockets: ArrayList<Socket> = arrayListOf();
for (i in addresses.indices) {
for (i in sortedAddresses.indices) {
sockets.add(Socket());
}
@@ -244,7 +253,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
var connectedSocket: Socket? = null;
val threads: ArrayList<Thread> = arrayListOf();
for (i in 0 until sockets.size) {
val address = addresses[i];
val address = sortedAddresses[i];
val socket = sockets[i];
val thread = Thread {
try {
@@ -1,13 +1,13 @@
package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol
import kotlin.math.abs
import kotlin.math.min
@@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
}
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
val urlData = if (this.startsWith("polycentric://")) {
this.substring("polycentric://".length)
} else this;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
addServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
for (pair in exceptions) {
val server = pair.key
val exception = pair.value
StateAnnouncement.instance.registerAnnouncement(
"backfill-failed",
"Backfill failed",
"Failed to backfill server $server. $exception",
AnnouncementType.SESSION_RECURRING
);
Logger.e("Backfill", "Failed to backfill server $server.", exception)
}
}
@@ -7,6 +7,9 @@ import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring
inline fun <reified T> Any.assume(): T?{
@@ -33,13 +36,37 @@ fun Boolean?.toYesNo(): String {
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
"[${toString()}]"
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
val index = hostAddr.indexOf('%')
if (index != -1) {
val addrPart = hostAddr.substring(0, index)
val scopeId = hostAddr.substring(index + 1)
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
} else {
"[$hostAddr]"
}
}
is Inet4Address -> {
toString()
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
}
else -> {
throw Exception("Invalid address type")
}
}
}
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
}
@@ -2,10 +2,30 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.*
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
//V8
@@ -24,6 +44,10 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
return handler(this);
}
inline fun V8Value.getSourcePlugin(): V8Plugin? {
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
}
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
@@ -89,7 +113,29 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
}
inline fun V8Plugin.ensureIsBusy() {
this.let {
if (!it.isThreadAlreadyBusy()) {
//throw IllegalStateException("Tried to access V8Plugin without busy");
val stacktrace = Thread.currentThread().stackTrace;
Logger.w("Extensions_V8",
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
", " + stacktrace.drop(4)?.firstOrNull().toString() +
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
);
}
}
}
inline fun V8Value.ensureIsBusy() {
this?.getSourcePlugin()?.let {
it.ensureIsBusy();
}
}
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
if(false)
ensureIsBusy();
return when(T::class) {
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
Int::class -> {
@@ -146,4 +192,170 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop));
return map;
}
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
val latch = CountDownLatch(1);
var promiseResult: T? = null;
var promiseException: Throwable? = null;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message);
else {
if(p0 is V8ValueObject)
p0.setWeak();
promiseResult = p0 as T;
}
latch.countDown();
}
override fun onRejected(p0: V8Value?) {
promiseException = p0?.toException(plugin.config);
latch.countDown();
}
override fun onCatch(p0: V8Value?) {
promiseException = p0?.toException(plugin.config);
latch.countDown();
}
});
}
plugin.registerPromise(this) {
promiseException = CancellationException("Cancelled by system");
latch.countDown();
}
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
if(!promise.isPending) {
try {
Logger.i("V8", "V8Promise resolved synchronously");
if(promise.isFulfilled)
promiseResult = promise.getResult<T>();
else
promiseException = promise.getResult<V8Value>().toException(plugin.config);
}
catch(ex: Throwable) {
promiseException = ex;
}
}
else {
plugin.unbusy {
latch.await();
}
}
if(promiseException != null)
throw promiseException!!;
return promiseResult!!;
}
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val underlyingDef = CompletableDeferred<T>();
val def = if(this.has("estDuration"))
V8Deferred(underlyingDef,
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(underlyingDef);
if(def.estDuration > 0)
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
val promise = this;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."));
}
});
}
plugin.registerPromise(promise) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
fun V8Value.toException(config: IV8PluginConfig): Throwable {
val p0 = this;
if(p0 is V8ValueObject) {
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
?: p0.getOrDefault(config, "message", "Promise Exception", "");
return Exception("Promise Failed: " + pluginType + msg);
}
else if(p0 is V8ValueString)
return Exception("Promise Failed:" + p0.value);
else
return NotImplementedError("onCatch promise not implemented..");
}
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
var amount = -1;
for(def in defs)
amount = Math.max(amount, def.estDuration);
val def = scope.async {
val results = defs.map { it.await() };
return@async conversion(results);
}
return V8Deferred(def, amount);
}
}
}
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result as T;
}
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
}
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result;
}
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
return result;
}
return V8Deferred(CompletableDeferred(result));
}
@@ -0,0 +1,192 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.io.ByteStreams
import com.google.common.primitives.Ints
import com.google.common.primitives.Longs
import java.io.DataInput
import java.io.DataInputStream
import java.io.EOFException
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
class LittleEndianDataInputStream
/**
* Creates a `LittleEndianDataInputStream` that wraps the given stream.
*
* @param in the stream to delegate to
*/
(`in`: InputStream?) : FilterInputStream(Preconditions.checkNotNull(`in`)), DataInput {
/** This method will throw an [UnsupportedOperationException]. */
override fun readLine(): String {
throw UnsupportedOperationException("readLine is not supported")
}
@Throws(IOException::class)
override fun readFully(b: ByteArray) {
ByteStreams.readFully(this, b)
}
@Throws(IOException::class)
override fun readFully(b: ByteArray, off: Int, len: Int) {
ByteStreams.readFully(this, b, off, len)
}
@Throws(IOException::class)
override fun skipBytes(n: Int): Int {
return `in`.skip(n.toLong()).toInt()
}
@Throws(IOException::class)
override fun readUnsignedByte(): Int {
val b1 = `in`.read()
if (0 > b1) {
throw EOFException()
}
return b1
}
/**
* Reads an unsigned `short` as specified by [DataInputStream.readUnsignedShort],
* except using little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as an unsigned 16-bit integer in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readUnsignedShort(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
return Ints.fromBytes(0.toByte(), 0.toByte(), b2, b1)
}
/**
* Reads an integer as specified by [DataInputStream.readInt], except using little-endian
* byte order.
*
* @return the next four bytes of the input stream, interpreted as an `int` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readInt(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
return Ints.fromBytes(b4, b3, b2, b1)
}
/**
* Reads a `long` as specified by [DataInputStream.readLong], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `long` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readLong(): Long {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
val b5 = readAndCheckByte()
val b6 = readAndCheckByte()
val b7 = readAndCheckByte()
val b8 = readAndCheckByte()
return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1)
}
/**
* Reads a `float` as specified by [DataInputStream.readFloat], except using
* little-endian byte order.
*
* @return the next four bytes of the input stream, interpreted as a `float` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readFloat(): Float {
return java.lang.Float.intBitsToFloat(readInt())
}
/**
* Reads a `double` as specified by [DataInputStream.readDouble], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `double` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readDouble(): Double {
return java.lang.Double.longBitsToDouble(readLong())
}
@Throws(IOException::class)
override fun readUTF(): String {
return DataInputStream(`in`).readUTF()
}
/**
* Reads a `short` as specified by [DataInputStream.readShort], except using
* little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as a `short` in little-endian
* byte order.
* @throws IOException if an I/O error occurs.
*/
@Throws(IOException::class)
override fun readShort(): Short {
return readUnsignedShort().toShort()
}
/**
* Reads a char as specified by [DataInputStream.readChar], except using little-endian
* byte order.
*
* @return the next two bytes of the input stream, interpreted as a `char` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readChar(): Char {
return readUnsignedShort().toChar()
}
@Throws(IOException::class)
override fun readByte(): Byte {
return readUnsignedByte().toByte()
}
@Throws(IOException::class)
override fun readBoolean(): Boolean {
return readUnsignedByte() != 0
}
/**
* Reads a byte from the input stream checking that the end of file (EOF) has not been
* encountered.
*
* @return byte read from input
* @throws IOException if an error is encountered while reading
* @throws EOFException if the end of file (EOF) is encountered.
*/
@Throws(IOException::class, EOFException::class)
private fun readAndCheckByte(): Byte {
val b1 = `in`.read()
if (-1 == b1) {
throw EOFException()
}
return b1.toByte()
}
}
@@ -0,0 +1,144 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.primitives.Longs
import java.io.*
class LittleEndianDataOutputStream
/**
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
*
* @param out the stream to delegate to
*/
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
DataOutput {
@Throws(IOException::class)
override fun write(b: ByteArray, off: Int, len: Int) {
// Override slow FilterOutputStream impl
out.write(b, off, len)
}
@Throws(IOException::class)
override fun writeBoolean(v: Boolean) {
(out as DataOutputStream).writeBoolean(v)
}
@Throws(IOException::class)
override fun writeByte(v: Int) {
(out as DataOutputStream).writeByte(v)
}
@Deprecated(
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
)
@Throws(
IOException::class
)
override fun writeBytes(s: String) {
(out as DataOutputStream).writeBytes(s)
}
/**
* Writes a char as specified by [DataOutputStream.writeChar], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChar(v: Int) {
writeShort(v)
}
/**
* Writes a `String` as specified by [DataOutputStream.writeChars], except
* each character is written using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChars(s: String) {
for (i in 0 until s.length) {
writeChar(s[i].code)
}
}
/**
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
* using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeDouble(v: Double) {
writeLong(java.lang.Double.doubleToLongBits(v))
}
/**
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeFloat(v: Float) {
writeInt(java.lang.Float.floatToIntBits(v))
}
/**
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeInt(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte(),
(0xFF and (v shr 16)).toByte(),
(0xFF and (v shr 24)).toByte()
)
out.write(bytes)
}
/**
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeLong(v: Long) {
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
write(bytes, 0, bytes.size)
}
/**
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeShort(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte()
)
out.write(bytes)
}
@Throws(IOException::class)
override fun writeUTF(str: String) {
(out as DataOutputStream).writeUTF(str)
}
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
// It should flush itself if necessary.
@Throws(IOException::class)
override fun close() {
out.close()
}
}
@@ -2,11 +2,8 @@ package com.futo.platformplayer
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity
@@ -14,6 +11,7 @@ import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.SyncHomeActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@@ -26,19 +24,19 @@ import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.AdvancedField
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.stripe.android.customersheet.injection.CustomerSheetViewModelModule_Companion_ContextFactory.context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -63,6 +61,15 @@ class Settings : FragmentedStorageFileJson() {
@Transient
val onTabsChanged = Event0();
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
@FormFieldButton(R.drawable.ic_update)
fun syncGrayjay() {
SettingsActivity.getActivity()?.let {
it.startActivity(Intent(it, SyncHomeActivity::class.java))
}
}
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
@FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() {
@@ -140,7 +147,6 @@ class Settings : FragmentedStorageFileJson() {
fun import() {
val act = SettingsActivity.getActivity() ?: return;
val intent = MainActivity.getImportOptionsIntent(act);
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
act.startActivity(intent);
}
@@ -172,6 +178,10 @@ class Settings : FragmentedStorageFileJson() {
}
}*/
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
var advancedSettings: Boolean = false;
@FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings();
@Serializable
@@ -193,6 +203,8 @@ class Settings : FragmentedStorageFileJson() {
8 -> "zh";
9 -> "ru";
10 -> "ar";
11 -> "it";
12 -> "tr";
else -> null
}
}
@@ -202,7 +214,7 @@ class Settings : FragmentedStorageFileJson() {
var home = HomeSettings();
@Serializable
class HomeSettings {
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
@DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1;
@@ -213,10 +225,16 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL;
}
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
var showHomeFilters: Boolean = true;
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@@ -245,12 +263,17 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
var hidefromSearch: Boolean = false;
fun getSearchFeedStyle(): FeedStyle {
if(searchFeedStyle == 0)
@@ -266,6 +289,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
}
@@ -288,16 +312,23 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@@ -328,13 +359,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@@ -350,7 +384,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings();
@Serializable
class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
@DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0;
@@ -374,6 +408,8 @@ class Settings : FragmentedStorageFileJson() {
else -> null
}
}
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true;
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@@ -409,23 +445,22 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true;
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
var autoRotate: Int = 2;
@AdvancedField
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
fun isAutoRotate() = (autoRotate == 1 && !StatePlayer.instance.rotationLock) || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate() && !StatePlayer.instance.rotationLock);
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2;
fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
@AdvancedField
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
@DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1;
@@ -452,14 +487,10 @@ class Settings : FragmentedStorageFileJson() {
};
}
@AdvancedField
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true;
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
var backgroundSwitchToAudio: Boolean = true;
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1;
@@ -471,14 +502,121 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 14)
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 15)
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
var allowVideoToGoUnderCutout: Boolean = true;
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
@DropdownFieldOptionsId(R.array.seek_offset_duration)
var seekOffset: Int = 2;
fun getSeekOffset(): Long {
return when(seekOffset) {
0 -> 3_000L;
1 -> 5_000L;
2 -> 10_000L;
3 -> 20_000L;
4 -> 30_000L;
5 -> 60_000L;
else -> 10_000L;
}
}
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
@DropdownFieldOptionsId(R.array.min_playback_speed)
var minimumPlaybackSpeed: Int = 0;
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.max_playback_speed)
var maximumPlaybackSpeed: Int = 2;
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.step_playback_speed)
var stepPlaybackSpeed: Int = 1;
fun getPlaybackSpeedStep(): Double {
return when(stepPlaybackSpeed) {
0 -> 0.05
1 -> 0.1
2 -> 0.25
else -> 0.1;
}
}
fun getPlaybackSpeeds(): List<Double> {
val playbackSpeeds = mutableListOf<Double>();
playbackSpeeds.add(1.0);
val minSpeed = when(minimumPlaybackSpeed) {
0 -> 0.25
1 -> 0.5
2 -> 1.0
else -> 0.25
}
val maxSpeed = when(maximumPlaybackSpeed) {
0 -> 2.0
1 -> 2.25
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 2.25;
}
var testSpeed = 1.0;
while(testSpeed > minSpeed) {
val nextSpeed = (testSpeed - 0.25) as Double;
testSpeed = Math.max(nextSpeed, minSpeed);
playbackSpeeds.add(testSpeed);
}
testSpeed = 1.0;
while(testSpeed < maxSpeed) {
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
testSpeed = Math.min(nextSpeed, maxSpeed);
playbackSpeeds.add(testSpeed);
}
playbackSpeeds.sort();
return playbackSpeeds;
}
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
var holdPlaybackSpeed: Int = 4;
fun getHoldPlaybackSpeed(): Double {
return when(holdPlaybackSpeed) {
0 -> 1.0
1 -> 1.25
2 -> 1.5
3 -> 1.75
4 -> 2.0
5 -> 2.25
6 -> 2.5
7 -> 2.75
8 -> 3.0
else -> 2.0
}
}
@AdvancedField
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
var shortsPregenerate: Boolean = false;
@AdvancedField
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
@FormFieldWarning(R.string.shorts_fit_video_warning)
var shortsFitVideo: Boolean = false;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -494,6 +632,10 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
@@ -527,10 +669,12 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true;
@AdvancedField
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
@DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3;
@@ -545,7 +689,7 @@ class Settings : FragmentedStorageFileJson() {
class Browsing {
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var videoCache: Boolean = true;
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
}
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
@@ -560,10 +704,26 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true;
@AdvancedField
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: Boolean = false;
@AdvancedField
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
@Serializable(with = FlexibleBooleanSerializer::class)
var experimentalCasting: Boolean = false
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@@ -631,6 +791,11 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true;
@@ -831,12 +996,32 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun clearPayment() {
StatePayment.instance.clearLicenses();
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
fun viewLicenseStatus() {
SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
it.reloadSettings();
try {
if (StatePayment.instance.hasPaid) {
val paymentKey = StatePayment.instance.getPaymentKey()
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
} else {
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show license status dialog", e)
}
}
}
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
fun clearPayment() {
SettingsActivity.getActivity()?.let { context ->
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();
}
})
}
}
}
@@ -845,12 +1030,21 @@ class Settings : FragmentedStorageFileJson() {
var other = Other();
@Serializable
class Other {
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
var bypassRotationPrevention: Boolean = false;
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
var watchLaterAddStart: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
var polycentricLocalCache: Boolean = true;
}
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@@ -882,7 +1076,72 @@ class Settings : FragmentedStorageFileJson() {
var pan: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
var synchronization = Synchronization();
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = false;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false;
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
var connectDiscovered: Boolean = true;
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true;
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
var discoverThroughRelay: Boolean = true;
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
var pairThroughRelay: Boolean = true;
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
var connectThroughRelay: Boolean = true;
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
var connectLocalDirectThroughRelay: Boolean = true;
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
var localConnections: Boolean = true;
var syncServerUrl: String? = null;
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
@AdvancedField
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
fun configureSyncServer() {
SettingsActivity.getActivity()?.let { context ->
UIDialogs.showDialog(context, R.drawable.device_sync, false,
"Enter the url to your relay server",
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
null,
syncServerUrl ?: "",
"YourRelayServerDomain.com", 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Reset", {
syncServerUrl = null;
instance.save();
context.reloadSettings();
UIDialogs.toast("Sync server changes require a restart");
}, UIDialogs.ActionStyle.ACCENT),
UIDialogs.Action.withInput("Configure", {
syncServerUrl = it?.text
instance.save();
context.reloadSettings();
UIDialogs.toast("Sync server changes require a restart");
}, UIDialogs.ActionStyle.PRIMARY),
)
}
}
}
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
var info = Info();
@Serializable
class Info {
@@ -948,4 +1207,4 @@ class Settings : FragmentedStorageFileJson() {
}
}
//endregion
}
}
@@ -1,6 +1,7 @@
package com.futo.platformplayer
import android.content.Context
import android.content.Intent
import android.webkit.CookieManager
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
@@ -1,64 +0,0 @@
package com.futo.platformplayer
import android.app.Activity
import android.content.pm.ActivityInfo
import android.hardware.SensorManager
import android.view.OrientationEventListener
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SimpleOrientationListener(
private val activity: Activity,
private val lifecycleScope: CoroutineScope
) {
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private val stabilityThresholdTime = 500L
val onOrientationChanged = Event1<Int>()
private val orientationListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
val newOrientation = when {
orientation in 45..134 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
orientation in 135..224 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
orientation in 225..314 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
orientation in 315..360 || orientation in 0..44 -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else -> lastOrientation
}
if (newOrientation != lastStableOrientation) {
lastStableOrientation = newOrientation
lifecycleScope.launch(Dispatchers.Main) {
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
}
}
}
}
}
init {
orientationListener.enable()
lastOrientation = activity.resources.configuration.orientation
}
fun stopListening() {
orientationListener.disable()
}
companion object {
private val TAG = "SimpleOrientationListener"
}
}
@@ -5,7 +5,9 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
import android.util.TypedValue
import android.view.Gravity
@@ -111,8 +113,8 @@ class UIDialogs {
currentDialog.code,
currentDialog.defaultCloseAction,
*currentDialog.actions.map {
return@map Action(it.text, {
it.action();
return@map Action.withInput(it.text, { str ->
it.invokeAction(str);
multiShowDialog(context, dialogDescriptor.drop(1), finally);
}, it.style);
}.toTypedArray());
@@ -198,34 +200,51 @@ class UIDialogs {
dialog.show();
}
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view);
builder.setCancelable(defaultCloseAction > -2);
val dialog = builder.create();
registerDialogOpened(dialog);
view.findViewById<ImageView>(R.id.dialog_icon).apply {
this.setImageResource(icon);
if(animated)
this.drawable.assume<Animatable, Unit> { it.start() };
}
view.findViewById<TextView>(R.id.dialog_text).apply {
this.text = text;
};
view.findViewById<TextView>(R.id.dialog_text_details).apply {
if(textDetails == null)
if (textDetails == null)
this.visibility = View.GONE;
else
else {
this.text = textDetails;
}
};
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
inputView.apply {
if (input == null && placeholder == null) this.visibility = View.GONE;
else {
this.text = input ?: "";
this.hint = placeholder ?: "";
this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
}
};
view.findViewById<TextView>(R.id.dialog_text_code).apply {
if(code == null)
this.visibility = View.GONE;
if (code == null) this.visibility = View.GONE;
else {
this.text = code;
this.movementMethod = ScrollingMovementMethod.getInstance();
this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
}
};
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
@@ -243,7 +262,7 @@ class UIDialogs {
buttonView.textSize = 14f;
buttonView.typeface = resources.getFont(R.font.inter_regular);
buttonView.text = act.text;
buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); };
when(act.style) {
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
@@ -268,12 +287,13 @@ class UIDialogs {
};
dialog.setOnCancelListener {
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
actions[defaultCloseAction].action();
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString()));
}
dialog.setOnDismissListener {
registerDialogClosed(dialog);
}
dialog.show();
return dialog;
}
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
@@ -311,7 +331,11 @@ class UIDialogs {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
retryAction?.invoke();
try {
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY)
);
else
@@ -325,7 +349,11 @@ class UIDialogs {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
retryAction?.invoke();
try {
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY)
);
}
@@ -348,6 +376,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);
@@ -360,8 +395,8 @@ class UIDialogs {
}
}
fun showChangelogDialog(context: Context, lastVersion: Int) {
val dialog = ChangelogDialog(context);
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
val dialog = ChangelogDialog(context, changelogs);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
@@ -401,7 +436,7 @@ class UIDialogs {
}
fun showCastingDialog(context: Context) {
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
val d = StateCasting.instance.activeDevice;
if (d != null) {
val dialog = ConnectedCastingDialog(context);
@@ -409,6 +444,7 @@ class UIDialogs {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
} else {
@@ -421,21 +457,24 @@ class UIDialogs {
if (c is Activity) {
dialog.setOwnerActivity(c);
}
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
}
fun showCastingTutorialDialog(context: Context) {
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
val dialog = CastingHelpDialog(context);
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showCastingAddDialog(context: Context) {
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
val dialog = CastingAddDialog(context);
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
@@ -508,17 +547,36 @@ class UIDialogs {
}
class Action {
val text: String;
val action: ()->Unit;
val action: ((DialogResult?)->Unit);
val style: ActionStyle;
var center: Boolean;
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text;
this.action = { action() };
this.style = style;
this.center = center;
}
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text;
this.action = action;
this.style = style;
this.center = center;
}
fun invokeAction(input: DialogResult? = null) {
this.action(input);
}
companion object {
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
return Action(text, action, style, center);
}
}
}
class DialogResult(
val text: String?
);
enum class ActionStyle {
NONE,
PRIMARY,
@@ -4,8 +4,14 @@ import android.app.NotificationManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
@@ -25,6 +31,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
@@ -36,6 +43,9 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.parsers.HLS.MediaRendition
import com.futo.platformplayer.parsers.HLS.StreamInfo
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
@@ -62,6 +72,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import androidx.core.net.toUri
class UISlideOverlays {
companion object {
@@ -78,6 +90,36 @@ class UISlideOverlays {
return menu;
}
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
UISlideOverlays.showOverlay(container, "Queue options", null, {
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text.trim()
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
StatePlayer.instance.saveQueueAsPlaylist(text);
UIDialogs.appToast("Playlist [${text}] created");
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
}, false));
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>();
@@ -87,115 +129,163 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts;
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
val menu = SlideUpMenuOverlay(
container.context,
container,
"Subscription Settings",
null,
true,
listOf()
);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) {
items.addAll(listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_notifications,
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
},
invokeParent = false
),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()) else null,
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuRecycler(container.context, "as") {
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if(it is SubscriptionGroup.Selectable) {
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
?: return@subscribe;
groups.clear();
if(it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
withContext(Dispatchers.Main) {
items.addAll(
listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_notifications,
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications =
menu?.selectOption(null, "notifications", true, true)
?: subscription.doNotifications;
},
invokeParent = false
),
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
.isNotEmpty()
)
SlideUpMenuGroup(
container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()
) else null,
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
.isNotEmpty()
)
SlideUpMenuRecycler(container.context, "as") {
val groups =
ArrayList<SubscriptionGroup>(
StateSubscriptionGroups.instance.getSubscriptionGroups()
.map {
SubscriptionGroup.Selectable(
it,
it.urls.contains(subscription.channel.url)
)
}
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if (it is SubscriptionGroup.Selectable) {
val actualGroup =
StateSubscriptionGroups.instance.getSubscriptionGroup(
it.id
)
?: return@subscribe;
groups.clear();
if (it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
container.context,
R.drawable.ic_live_tv,
"Livestreams",
"Check for livestreams",
tag = "fetchLive",
call = {
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Streams",
"Check for streams",
tag = "fetchStreams",
call = {
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Videos",
"Check for videos",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Content",
"Check for content",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
StateSubscriptionGroups.instance.updateSubscriptionGroup(
actualGroup
);
groups.addAll(
StateSubscriptionGroups.instance.getSubscriptionGroups()
.map {
SubscriptionGroup.Selectable(
it,
it.urls.contains(subscription.channel.url)
)
}
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(
container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()
),
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
container.context,
R.drawable.ic_live_tv,
"Livestreams",
"Check for livestreams",
tag = "fetchLive",
call = {
subscription.doFetchLive =
menu?.selectOption(null, "fetchLive", true, true)
?: subscription.doFetchLive;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Streams",
"Check for streams",
tag = "fetchStreams",
call = {
subscription.doFetchStreams =
menu?.selectOption(null, "fetchStreams", true, true)
?: subscription.doFetchStreams;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Videos",
"Check for videos",
tag = "fetchVideos",
call = {
subscription.doFetchVideos =
menu?.selectOption(null, "fetchVideos", true, true)
?: subscription.doFetchVideos;
},
invokeParent = false
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Content",
"Check for content",
tag = "fetchVideos",
call = {
subscription.doFetchVideos =
menu?.selectOption(null, "fetchVideos", true, true)
?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts =
menu?.selectOption(null, "fetchPosts", true, true)
?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
@@ -203,61 +293,82 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel);
}, false)*/
).filterNotNull());
).filterNotNull()
);
menu.setItems(items);
menu.setItems(items);
if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
if(subscription.doFetchLive)
menu.selectOption(null, "fetchLive", true, true);
if(subscription.doFetchStreams)
menu.selectOption(null, "fetchStreams", true, true);
if(subscription.doFetchVideos)
menu.selectOption(null, "fetchVideos", true, true);
if(subscription.doFetchPosts)
menu.selectOption(null, "fetchPosts", true, true);
if (subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
if (subscription.doFetchLive)
menu.selectOption(null, "fetchLive", true, true);
if (subscription.doFetchStreams)
menu.selectOption(null, "fetchStreams", true, true);
if (subscription.doFetchVideos)
menu.selectOption(null, "fetchVideos", true, true);
if (subscription.doFetchPosts)
menu.selectOption(null, "fetchPosts", true, true);
menu.onOK.subscribe {
subscription.save();
menu.hide(true);
menu.onOK.subscribe {
subscription.save();
menu.hide(true);
if(subscription.doNotifications && !originalNotif) {
val mainContext = StateApp.instance.contextOrNull;
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
if (subscription.doNotifications && !originalNotif) {
val mainContext = StateApp.instance.contextOrNull;
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast(
container.context,
"Enable 'Background Update' in settings for notifications to work"
);
if(mainContext is MainActivity) {
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
"You need to set a Background Updating interval for notifications", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Configure", {
val intent = Intent(mainContext, SettingsActivity::class.java);
intent.putExtra("query", mainContext.getString(R.string.background_update));
mainContext.startActivity(intent);
}, UIDialogs.ActionStyle.PRIMARY));
}
return@subscribe;
}
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
UIDialogs.toast(container.context, "Android notifications are disabled");
if(mainContext is MainActivity) {
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
if (mainContext is MainActivity) {
UIDialogs.showDialog(
mainContext,
R.drawable.ic_settings,
"Background Updating Required",
"You need to set a Background Updating interval for notifications",
null,
0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Configure", {
val intent = Intent(
mainContext,
SettingsActivity::class.java
);
intent.putExtra(
"query",
mainContext.getString(R.string.background_update)
);
mainContext.startActivity(intent);
}, UIDialogs.ActionStyle.PRIMARY)
);
}
return@subscribe;
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
UIDialogs.toast(
container.context,
"Android notifications are disabled"
);
if (mainContext is MainActivity) {
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
}
}
}
}
};
menu.onCancel.subscribe {
subscription.doNotifications = originalNotif;
subscription.doFetchLive = originalLive;
subscription.doFetchStreams = originalStream;
subscription.doFetchVideos = originalVideo;
subscription.doFetchPosts = originalPosts;
};
};
menu.onCancel.subscribe {
subscription.doNotifications = originalNotif;
subscription.doFetchLive = originalLive;
subscription.doFetchStreams = originalStream;
subscription.doFetchVideos = originalVideo;
subscription.doFetchPosts = originalPosts;
};
menu.setOk("Save");
menu.setOk("Save");
menu.show();
menu.show();
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show subscription overlay.", e)
}
}
@@ -268,6 +379,7 @@ class UISlideOverlays {
}
@OptIn(UnstableApi::class)
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@@ -279,6 +391,8 @@ class UISlideOverlays {
val masterPlaylistContent = masterPlaylistResponse.body?.string()
?: throw Exception("Master playlist content is empty")
val resolvedPlaylistUrl = masterPlaylistResponse.url
val videoButtons = arrayListOf<SlideUpMenuItem>()
val audioButtons = arrayListOf<SlideUpMenuItem>()
//TODO: Implement subtitles
@@ -291,53 +405,103 @@ class UISlideOverlays {
val masterPlaylist: HLS.MasterPlaylist
try {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
.parse(sourceUrl.toUri(), inputStream)
masterPlaylist.getAudioSources().forEach { it ->
if (playlist is HlsMediaPlaylist) {
if (source is IHLSManifestAudioSource) {
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
variant.name,
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedAudioVariant = variant
slideUpMenuOverlay.selectOption(audioButtons, variant)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
} else {
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
variant.name,
"${variant.width}x${variant.height}",
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedVideoVariant = variant
slideUpMenuOverlay.selectOption(videoButtons, variant)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
} else if (playlist is HlsMultivariantPlaylist) {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
}, false))
}*/
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false))
}*/
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
}
val newItems = arrayListOf<View>()
@@ -365,11 +529,11 @@ class UISlideOverlays {
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
withContext(Dispatchers.Main) {
if (source is IHLSManifestSource) {
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide()
} else {
@@ -416,7 +580,7 @@ class UISlideOverlays {
}
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
listOf(listOf(SlideUpMenuItem(
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.none),
@@ -429,7 +593,7 @@ class UISlideOverlays {
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)) +
)) else listOf()) +
videoSources
.filter { it.isDownloadable() }
.map {
@@ -651,6 +815,10 @@ class UISlideOverlays {
}
}
}
if(!Settings.instance.downloads.shouldDownload()) {
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
"(You can change this in settings)", true);
}
}
};
return menu.apply { show() };
@@ -879,6 +1047,12 @@ class UISlideOverlays {
val items = arrayListOf<View>();
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",
@@ -888,7 +1062,8 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
);
@@ -899,17 +1074,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 && !video.isLive)
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,29 +1112,33 @@ 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",
SlideUpMenuItem(container.context,
SlideUpMenuGroup(
container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.add_to_queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
SlideUpMenuItem(container.context,
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem(
container.context,
R.drawable.ic_history,
container.context.getString(R.string.add_to_history),
"Mark as watched",
tag = "history",
call = { StateHistory.instance.markAsWatched(video); }),
));
));
val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(
@@ -983,7 +1163,8 @@ class UISlideOverlays {
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(playlist.id, video);
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
}
@@ -1010,7 +1191,8 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
);
@@ -1020,28 +1202,28 @@ class UISlideOverlays {
val queue = StatePlayer.instance.getQueue();
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(container.context,
SlideUpMenuGroup(
container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER,
"${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
))
call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false);
else
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}),
)
);
val playlistItems = arrayListOf<SlideUpMenuItem>();
@@ -1067,7 +1249,8 @@ class UISlideOverlays {
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
StatePlaylists.instance.addToPlaylist(playlist.id, video);
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
}
@@ -1078,8 +1261,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
}
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
overlay.show();
return overlay;
}
@@ -1109,7 +1292,7 @@ class UISlideOverlays {
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
tag = "",
call = {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
val selected = it
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
@@ -1117,7 +1300,7 @@ class UISlideOverlays {
.toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
}
});
},
invokeParent = false
))
@@ -1125,29 +1308,40 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
}
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
val selection: MutableList<Any> = mutableListOf();
var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
options.map { SlideUpMenuItem(
listOf(
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
).filterNotNull() +
(options.map { SlideUpMenuItem(
container.context,
R.drawable.ic_move_up,
it.first,
"",
tag = it.second,
call = {
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
if(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second))
if(!selection.contains(it.second)) {
selection.add(it.second);
} else
if(overlayItem != null) {
overlayItem.setSubText(selection.indexOf(it.second).toString());
}
}
} else {
selection.remove(it.second);
if(overlayItem != null) {
overlayItem.setSubText("");
}
}
},
invokeParent = false
)
});
}));
overlay.onOK.subscribe {
onOrdered.invoke(selection);
overlay.hide();
@@ -26,12 +26,24 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.File
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.time.OffsetDateTime
import java.util.*
import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String {
@@ -63,7 +75,14 @@ fun warnIfMainThread(context: String) {
}
fun ensureNotMainThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
val isMainLooper = try {
Looper.myLooper() == Looper.getMainLooper()
} catch (e: Throwable) {
//Ignore, for unit tests where its not mocked
false
}
if (isMainLooper) {
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
throw IllegalStateException("Cannot run on main thread")
}
@@ -230,4 +249,213 @@ fun String.decodeUnicode(): String {
i++
}
return sb.toString()
}
}
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
val newArrResult = targetArr.toMutableList();
for(missing in missingToMerge) {
val newIndex = findNewIndex(toMerge, newArrResult, missing);
newArrResult.add(newIndex, missing);
}
return newArrResult;
}
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
var originalIndex = originalArr.indexOf(item);
var newIndex = -1;
for(i in originalIndex-1 downTo 0) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr + 1;
break;
}
}
if(newIndex < 0) {
for(i in originalIndex+1 until originalArr.size) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr - 1;
break;
}
}
}
if(newIndex < 0)
return newArr.size;
else
return newIndex;
}
fun ByteBuffer.toUtf8String(): String {
val remainingBytes = ByteArray(remaining())
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
fun generateReadablePassword(length: Int): String {
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
val secureRandom = SecureRandom()
val randomBytes = ByteArray(length)
secureRandom.nextBytes(randomBytes)
val sb = StringBuilder(length)
for (byte in randomBytes) {
val index = (byte.toInt() and 0xFF) % validChars.length
sb.append(validChars[index])
}
return sb.toString()
}
fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val gzipTimeStart = OffsetDateTime.now();
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(this)
}
val result = outputStream.toByteArray();
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
return result;
}
fun ByteArray.fromGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val inputStream = ByteArrayInputStream(this)
val outputStream = ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
}
fun findCandidateAddresses(): List<InetAddress> {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.sortedWith(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
).map { it.second.address }
}
fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.minWithOrNull(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
)?.second?.address
}
private fun isUsableInterface(nif: NetworkInterface): Boolean {
val name = nif.name.lowercase()
return try {
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
nif.isUp
&& !nif.isLoopback
&& !nif.isPointToPoint
&& !nif.isVirtual
&& !name.startsWith("docker")
&& !name.startsWith("veth")
&& !name.startsWith("br-")
&& !name.startsWith("virbr")
&& !name.startsWith("vmnet")
&& !name.startsWith("tun")
&& !name.startsWith("tap")
} catch (e: SocketException) {
false
}
}
private fun isUsableAddress(addr: InetAddress): Boolean {
return when {
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
addr.isLoopbackAddress -> false
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
addr.isMulticastAddress -> false
else -> true
}
}
private fun interfaceScore(nif: NetworkInterface): Int {
val name = nif.name.lowercase()
return when {
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
name.startsWith("eth") || name.contains("ethernet") -> 0
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
name.contains("wi-fi") || name.contains("wifi") -> 1
else -> 2
}
}
fun addressScore(addr: InetAddress): Int {
return when (addr) {
is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF }
when {
octets[0] == 10 -> 0 // 10/8
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.1631/12
else -> 1 // public IPv4
}
}
is Inet6Address -> {
// ULA (fc00::/7) vs global vs others
val b0 = addr.address[0].toInt() and 0xFF
when {
(b0 and 0xFE) == 0xFC -> 2 // ULA
(b0 and 0xE0) == 0x20 -> 3 // global
else -> 4
}
}
else -> Int.MAX_VALUE
}
}
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
@@ -10,11 +10,13 @@ import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.zxing.integration.android.IntentIntegrator
class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton;
lateinit var _overlayContainer: FrameLayout;
lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton;
@@ -54,6 +56,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
setContentView(R.layout.activity_add_source_options);
setNavigationBarColorAndIcons();
_overlayContainer = findViewById(R.id.overlay_container);
_buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr);
@@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
}
_buttonURL.onClick.subscribe {
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
val content = nameInput.text;
val url = if (content.startsWith("https://")) {
content
} else if (content.startsWith("grayjay://plugin/")) {
content.substring("grayjay://plugin/".length)
} else {
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
return@showOverlay;
}
val intent = Intent(this, AddSourceActivity::class.java).apply {
data = Uri.parse(url);
};
startActivity(intent);
}, nameInput)
}
}
}
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
@@ -74,9 +75,26 @@ class LoginActivity : AppCompatActivity() {
finish();
};
var isFirstLoad = true;
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
var currentScale = 100;
var currentDesktop = false;
webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: "");
if(loginWarnings.size > 0 && url != null) {
synchronized(loginWarnings) {
val warning = loginWarnings.find { url.matches(it.getRegex()) };
if(warning != null) {
if(warning.once == true)
loginWarnings.remove(warning);
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
UIDialogs.Action("Understood", {
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
if(!isFirstLoad)
return@subscribe;
isFirstLoad = false;
@@ -86,6 +104,35 @@ class LoginActivity : AppCompatActivity() {
//TODO: Find most reliable way to wait for page js to finish
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
}
/*
var specifiedScale = false;
var specifiedDesktop = false;
if(uiMods.size > 0 && url != null) {
synchronized(uiMods) {
val uimod = uiMods.find { url.matches(it.getRegex()) };
if(uimod != null) {
if(uimod.scale != null) {
currentScale =(uimod.scale * 100).toInt();
_webView.setInitialScale(currentScale);
specifiedScale = true;
}
if(uimod.desktop != null && uimod.desktop) {
_webView.settings.useWideViewPort = true;
specifiedDesktop = true;
}
}
}
}
if(!specifiedScale && currentScale != 100) {
currentScale = (100).toInt();
_webView.setInitialScale(currentScale);
}
if(!specifiedDesktop && currentDesktop) {
_webView.settings.useWideViewPort = false;
currentDesktop = false;
}
*/
}
_webView.settings.domStorageEnabled = true;
@@ -113,7 +160,7 @@ class LoginActivity : AppCompatActivity() {
companion object {
private val TAG = "LoginActivity";
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
private var _callback: ((SourceAuth?) -> Unit)? = null;
File diff suppressed because it is too large Load Diff
@@ -14,10 +14,12 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType
@@ -29,6 +31,9 @@ import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo
@@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
private lateinit var _loader: View
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons();
_buttonShare = findViewById(R.id.button_share);
_buttonCopy = findViewById(R.id.button_copy);
_imageQR = findViewById(R.id.image_qr);
_textQR = findViewById(R.id.text_qr);
_buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
_exportBundle = createExportBundle();
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
_imageQR.setImageBitmap(qrCodeBitmap);
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
_imageQR.visibility = View.INVISIBLE;
_textQR.visibility = View.INVISIBLE;
lifecycleScope.launch {
try {
val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle()
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
val qr = generateQRCode(bundle, dimension, dimension)
Pair(bundle, qr)
}
_exportBundle = pair.first
_imageQR.setImageBitmap(pair.second)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
}
_buttonShare.onClick.subscribe {
@@ -3,6 +3,7 @@ package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
@@ -10,15 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -27,6 +29,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
private lateinit var _profileName: EditText;
private lateinit var _buttonCreate: LinearLayout;
private lateinit var _loader: LoaderView;
private val TAG = "PolycentricCreateProfileActivity";
private var _creating = false;
@@ -43,6 +46,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help);
_profileName = findViewById(R.id.edit_profile_name);
_buttonCreate = findViewById(R.id.button_create_profile);
_loader = findViewById(R.id.loader);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
@@ -65,35 +69,49 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
return@setOnClickListener;
}
_profileName.isEnabled = false;
_buttonCreate.visibility = View.GONE;
_loader.start();
_loader.visibility = View.VISIBLE;
lifecycleScope.launch(Dispatchers.IO) {
val processHandle: ProcessHandle;
try {
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(ApiMethods.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
try {
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
}
}
try {
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
finally {
withContext(Dispatchers.Main) {
_profileName.isEnabled = true;
_buttonCreate.visibility = View.VISIBLE;
_loader.stop();
_loader.visibility = View.GONE;
}
}
withContext(Dispatchers.Main) {
@@ -8,6 +8,7 @@ import android.os.Bundle
import android.util.TypedValue
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
@@ -28,6 +29,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
private lateinit var _buttonNewProfile: BigButton;
private lateinit var _buttonImportProfile: BigButton;
private lateinit var _layoutButtons: LinearLayout;
private lateinit var _scroll: ScrollView;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -42,6 +44,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
_buttonNewProfile = findViewById(R.id.button_new_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile);
_layoutButtons = findViewById(R.id.layout_buttons);
_scroll = findViewById(R.id.scroll);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
@@ -78,6 +81,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
_layoutButtons.addView(profileButton, 0);
}
_scroll.invalidate();
_buttonHelp.setOnClickListener {
startActivity(Intent(this, PolycentricWhyActivity::class.java));
@@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
@@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
@@ -21,18 +21,20 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker
@@ -47,6 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
private lateinit var _editName: EditText;
private lateinit var _buttonExport: BigButton;
private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String;
@@ -68,10 +71,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export);
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay);
_textSystem = findViewById(R.id.text_system)
findViewById<TextView>(R.id.text_cta2).setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://harbor.social")))
}
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired();
finish();
@@ -92,6 +99,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java));
};
_buttonOpenHarborProfile.onClick.subscribe {
val processHandle = StatePolycentric.instance.processHandle!!;
processHandle?.let {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
}
}
_buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null);
startActivity(Intent(this, PolycentricHomeActivity::class.java));
@@ -108,6 +125,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
StatePolycentric.instance.setProcessHandle(null);
Store.instance.removeProcessSecret(processHandle.system);
PolycentricStorage.instance.removeProcessSecret(processHandle.system);
startActivity(Intent(this, PolycentricHomeActivity::class.java));
finish();
});
@@ -127,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) {
updateUI();
@@ -0,0 +1,166 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.LinkType
import com.futo.platformplayer.sync.internal.SyncSession
import com.futo.platformplayer.views.sync.SyncDeviceView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SyncHomeActivity : AppCompatActivity() {
private lateinit var _layoutDevices: LinearLayout
private lateinit var _layoutEmpty: LinearLayout
private val _viewMap: MutableMap<String, SyncDeviceView> = mutableMapOf()
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (StateApp.instance.contextOrNull == null) {
Logger.w(TAG, "No main activity, restarting main.")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
return
}
setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons()
_layoutDevices = findViewById(R.id.layout_devices)
_layoutEmpty = findViewById(R.id.layout_empty)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncPairActivity::class.java))
}
findViewById<LinearLayout>(R.id.button_show_pairing_code).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncShowPairingCodeActivity::class.java))
}
initializeDevices()
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { publicKey, session ->
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[publicKey]
if (!session.isAuthorized) {
if (view != null) {
_viewMap.remove(publicKey)
}
return@launch
}
if (view == null) {
val syncDeviceView = SyncDeviceView(this@SyncHomeActivity)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val v = updateDeviceView(syncDeviceView, publicKey, session)
_layoutDevices.addView(v, 0)
_viewMap[publicKey] = v
} else {
updateDeviceView(view, publicKey, session)
}
updateEmptyVisibility()
}
}
StateSync.instance.deviceRemoved.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[it]
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(it)
}
updateEmptyVisibility()
}
}
StateSync.instance.confirmStarted(this, onStarted = {
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
}
if (StateSync.instance.syncService?.relayConnected == false) {
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
}
if (StateSync.instance.syncService?.serverSocketStarted == false) {
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
}
}, onNotStarted = {
finish()
})
}
override fun onDestroy() {
super.onDestroy()
StateSync.instance.deviceUpdatedOrAdded.remove(this)
StateSync.instance.deviceRemoved.remove(this)
}
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
val authorized = session?.isAuthorized ?: false
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized")
return syncDeviceView
}
private fun updateEmptyVisibility() {
if (_viewMap.isNotEmpty()) {
_layoutEmpty.visibility = View.GONE
} else {
_layoutEmpty.visibility = View.VISIBLE
}
}
private fun initializeDevices() {
_layoutDevices.removeAllViews()
for (publicKey in StateSync.instance.getAll()) {
val syncDeviceView = SyncDeviceView(this)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val view = updateDeviceView(syncDeviceView, publicKey, StateSync.instance.getSession(publicKey))
_layoutDevices.addView(view)
_viewMap[publicKey] = view
}
updateEmptyVisibility()
}
companion object {
private const val TAG = "SyncHomeActivity"
}
}
@@ -0,0 +1,153 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.util.Base64
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
class SyncPairActivity : AppCompatActivity() {
private lateinit var _editCode: EditText
private lateinit var _layoutPairing: LinearLayout
private lateinit var _textPairingStatus: TextView
private lateinit var _layoutPairingSuccess: LinearLayout
private lateinit var _layoutPairingError: LinearLayout
private lateinit var _textError: TextView
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
scanResult?.let {
if (it.contents != null) {
_editCode.text.clear()
_editCode.text.append(it.contents)
pair(it.contents)
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_pair)
setNavigationBarColorAndIcons()
_editCode = findViewById(R.id.edit_code)
_layoutPairing = findViewById(R.id.layout_pairing)
_textPairingStatus = findViewById(R.id.text_pairing_status)
_layoutPairingSuccess = findViewById(R.id.layout_pairing_success)
_layoutPairingError = findViewById(R.id.layout_pairing_error)
_textError = findViewById(R.id.text_error)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_scan_qr).setOnClickListener {
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true);
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
integrator.setBarcodeImageEnabled(true)
integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent())
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
pair(_editCode.text.toString())
}
_layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE
finish()
}
_layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE
}
_layoutPairingSuccess.visibility = View.GONE
_layoutPairingError.visibility = View.GONE
}
fun pair(url: String) {
try {
_layoutPairing.visibility = View.VISIBLE
_textPairingStatus.text = "Parsing text..."
if (!url.startsWith("grayjay://sync/")) {
throw Exception("Not a valid URL: $url")
}
val deviceInfo: SyncDeviceInfo = Json.decodeFromString<SyncDeviceInfo>(Base64.decode(url.substring("grayjay://sync/".length), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).decodeToString())
if (StateSync.instance.isAuthorized(deviceInfo.publicKey)) {
throw Exception("This device is already paired")
}
_textPairingStatus.text = "Connecting..."
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
lifecycleScope.launch(Dispatchers.Main) {
if (complete != null) {
if (complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textError.text = message
_layoutPairingError.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
}
} else {
_textPairingStatus.text = message
}
}
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
_layoutPairingError.visibility = View.VISIBLE
if(e.message == "Failed to connect") {
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
}
else
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
}
}
} catch(e: Throwable) {
_layoutPairingError.visibility = View.VISIBLE
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
}
companion object {
private const val TAG = "SyncPairActivity"
}
}
@@ -0,0 +1,149 @@
package com.futo.platformplayer.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.NetworkInterface
class SyncShowPairingCodeActivity : AppCompatActivity() {
private lateinit var _textCode: TextView
private lateinit var _imageQR: ImageView
private lateinit var _textQR: TextView
private var _code: String? = null
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onDestroy() {
super.onDestroy()
activity = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity = this
setContentView(R.layout.activity_sync_show_pairing_code)
setNavigationBarColorAndIcons()
_textCode = findViewById(R.id.text_code)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_scan_qr)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_copy).setOnClickListener {
val code = _code ?: return@setOnClickListener
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
val clip = ClipData.newPlainText(getString(R.string.copied_text), code);
clipboard.setPrimaryClip(clip);
UIDialogs.toast(this, "Copied to clipboard")
}
val ips = getIPs()
val publicKey = StateSync.instance.syncService?.publicKey
val pairingCode = StateSync.instance.syncService?.pairingCode
if (publicKey == null || pairingCode == null) {
setCode("Public key or pairing code was not known, is sync enabled?")
} else {
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
}
fun setCode(code: String?) {
_code = code
_textCode.text = code
if (code == null) {
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
return
}
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
_imageQR.setImageBitmap(qrCodeBitmap)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
}
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
return bitMatrixToBitmap(bitMatrix);
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
val width = matrix.width;
val height = matrix.height;
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
}
}
return bmp;
}
private fun getIPs(): List<String> {
val ips = arrayListOf<String>()
for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) {
if (addr.isLoopbackAddress) {
continue
}
if (addr.address.size != 4) {
continue
}
addr.hostAddress?.let { ips.add(it) }
}
}
return ips
}
companion object {
private const val TAG = "SyncShowPairingCodeActivity"
var activity: SyncShowPairingCodeActivity? = null
private set
}
}
@@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.views.TargetTapLoaderView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
view.startLoader(10000)
lifecycleScope.launch {
delay(5000)
view.startLoader()
}
}
companion object {
@@ -5,6 +5,8 @@ import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.FragmentedStorage
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
@@ -63,7 +65,7 @@ open class ManagedHttpClient {
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
@@ -88,6 +90,7 @@ open class ManagedHttpClient {
}
fun tryHead(url: String): Map<String, String>? {
ensureNotMainThread()
try {
val result = head(url);
if(result.isOk)
@@ -102,7 +105,7 @@ open class ManagedHttpClient {
}
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
ensureNotMainThread()
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url);
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
@@ -298,6 +301,7 @@ open class ManagedHttpClient {
}
fun send(msg: String) {
ensureNotMainThread()
socket.send(msg);
}
@@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
current += bytesToSend.toLong()
if (current >= end) {
if (current > end) {
Logger.i(TAG, "Expected amount of bytes sent")
break
}
@@ -1,5 +1,6 @@
package com.futo.platformplayer.api.media
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@@ -12,6 +13,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
@@ -35,6 +37,11 @@ interface IPlatformClient {
*/
fun getHome(): IPager<IPlatformContent>
/**
* Gets the shorts feed
*/
fun getShorts(): IPager<IPlatformVideo>
//Search
/**
* Gets search suggestion for the provided query string
@@ -66,6 +73,11 @@ interface IPlatformClient {
*/
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages
/**
@@ -170,6 +182,10 @@ interface IPlatformClient {
* Retrieves the subscriptions of the currently logged in user
*/
fun getUserSubscriptions(): Array<String>;
/**
* Retrieves the history of the currently logged in user
*/
fun getUserHistory(): IPager<IPlatformContent>;
fun isClaimTypeSupported(claimType: Int): Boolean;
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.live.LiveEventComment
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.BatchedTaskHandler
import com.futo.platformplayer.logging.Logger
@@ -26,12 +27,17 @@ class LiveChatManager {
private val _emojiCache: EmojiCache = EmojiCache();
private val _pager: IPager<IPlatformLiveEvent>?;
private var _position: Long = 0;
private var _eventsPosition: Long = 0;
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
private var _startCounter = 0;
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
val isVOD get() = _pager is JSVODEventPager;
var viewCount: Long = 0
private set;
@@ -39,8 +45,24 @@ class LiveChatManager {
_scope = scope;
_pager = pager;
viewCount = initialViewCount;
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
handleEvents(pager.getResults());
if(pager is JSVODEventPager)
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
else
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
if(pager is JSVODEventPager) {
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
//TODO: Remove this once dripfeed is done properly
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
if(replayResults.size > 0) {
_eventsPosition = replayResults.maxOf { it.time };
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
}
else
_eventsPosition = _eventsPosition + 1500;
}
else
handleEvents(pager.getResults());
}
fun start() {
@@ -52,6 +74,10 @@ class LiveChatManager {
_startCounter++;
}
fun setVideoPosition(ms: Long) {
_position = ms;
}
fun getHistory(): List<IPlatformLiveEvent> {
synchronized(_history) {
return _history.toList();
@@ -85,13 +111,34 @@ class LiveChatManager {
try {
while(_startCounter == counter) {
var nextInterval = 1000L;
if(_pager is JSVODEventPager && _eventsPosition > _position) {
delay(500);
continue;
}
try {
if(_pager == null || !_pager.hasMorePages())
return@launch;
_pager.nextPage();
val newEvents = _pager.getResults();
val newEvents = if(_pager is JSVODEventPager) {
val requestPosition = _position;
_pager.nextPage(requestPosition.toInt());
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
if(replayResults.size > 0) {
_eventsPosition = replayResults.maxOf { it.time };
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
}
else
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
replayResults;
}
else {
_pager.nextPage();
_pager.getResults();
}
if(_pager is JSLiveEventPager)
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
else if(_pager is JSVODEventPager)
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
if(newEvents.size > 0)
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
@@ -20,7 +20,8 @@ data class PlatformClientCapabilities(
val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false
val hasGetContentRecommendations: Boolean = false,
val hasGetUserHistory: Boolean = false
) {
}
@@ -14,14 +14,16 @@ class PlatformClientPool {
private var _poolCounter = 0;
private val _poolName: String?;
private val _privatePool: Boolean;
private val _isolatedInitialization: Boolean
var isDead: Boolean = false
private set;
val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) {
_poolName = name;
_privatePool = privatePool;
_isolatedInitialization = isolatedInitialization
if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started");
@@ -32,8 +34,10 @@ class PlatformClientPool {
isDead = true;
onDead.emit(parentClient, this);
for(clientPair in _pool) {
clientPair.key.disable();
synchronized(_pool) {
for (clientPair in _pool) {
clientPair.key.disable();
}
}
};
}
@@ -53,7 +57,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool);
reserved = _parent.getCopy(_privatePool, _isolatedInitialization);
reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
@@ -44,6 +45,7 @@ class PlatformID {
val NONE = PlatformID("Unknown", null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
value.ensureIsBusy();
val contextName = "PlatformID";
return PlatformID(
value.getOrThrow(config, "platform", contextName),
@@ -7,13 +7,15 @@ class PlatformMultiClientPool {
private var _isFake = false;
private var _privatePool = false;
private val _isolatedInitialization: Boolean
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) {
_name = name;
_maxCap = if(maxCap > 0)
maxCap
else 99;
_privatePool = isPrivatePool;
_isolatedInitialization = isolatedInitialization
}
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@@ -21,7 +23,7 @@ class PlatformMultiClientPool {
return parentClient;
val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply {
this.onDead.subscribe { _, pool ->
synchronized(_clientPools) {
if(_clientPools[parentClient] == pool)
@@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
class Serializer {
companion object {
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
}
}
@@ -2,7 +2,11 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -30,6 +34,7 @@ open class PlatformAuthorLink {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
value.ensureIsBusy();
if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value);
@@ -42,4 +47,21 @@ open class PlatformAuthorLink {
);
}
}
}
interface IPlatformChannelContent : IPlatformContent {
val thumbnail: String?
val subscribers: Long?
}
open class JSChannelContent : JSContent, IPlatformChannelContent {
override val contentType: ContentType get() = ContentType.CHANNEL
override val thumbnail: String?
override val subscribers: Long?
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Channel";
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
}
}
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
value.ensureIsBusy();
val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context),
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.expectV8Variant
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -30,6 +31,7 @@ class ResultCapabilities(
const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED";
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
const val TYPE_SHORTS = "SHORTS";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
@@ -45,6 +47,7 @@ class ResultCapabilities(
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
val contextName = "ResultCapabilities";
value.ensureIsBusy();
return ResultCapabilities(
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
@@ -68,6 +71,7 @@ class FilterGroup(
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
value.ensureIsBusy();
return FilterGroup(
value.getString("name"),
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
@@ -89,6 +93,7 @@ class FilterCapability(
companion object {
fun fromV8(obj: V8ValueObject): FilterCapability {
obj.ensureIsBusy();
val value = obj.get("value") as V8Value;
return FilterCapability(
obj.getString("name"),
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -31,6 +32,7 @@ class Thumbnails {
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
value.ensureIsBusy();
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
@@ -0,0 +1,9 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
interface IPlatformArticle: IPlatformContent {
val summary: String?;
val thumbnails: Thumbnails?;
}
@@ -0,0 +1,12 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
val segments: List<IJSArticleSegment>;
val rating : IRating;
}
@@ -0,0 +1,63 @@
package com.futo.platformplayer.api.media.models.comments
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.ratings.RatingType
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Deferred
import java.time.OffsetDateTime
class LazyComment: IPlatformComment {
private var _commentDeferred: Deferred<IPlatformComment>;
private var _commentLoaded: IPlatformComment? = null;
private var _commentException: Throwable? = null;
override val contextUrl: String
get() = _commentLoaded?.contextUrl ?: "";
override val author: PlatformAuthorLink
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
override val message: String
get() = _commentLoaded?.message ?: "";
override val rating: IRating
get() = _commentLoaded?.rating ?: RatingLikes(0);
override val date: OffsetDateTime?
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
override val replyCount: Int?
get() = _commentLoaded?.replyCount ?: 0;
val isAvailable: Boolean get() = _commentLoaded != null;
private var _uiHandler: ((LazyComment)->Unit)? = null;
constructor(commentDeferred: Deferred<IPlatformComment>) {
_commentDeferred = commentDeferred;
_commentDeferred.invokeOnCompletion {
if(it == null) {
_commentLoaded = commentDeferred.getCompleted();
Logger.i("LazyComment", "Resolved comment");
}
else {
_commentException = it;
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
}
_uiHandler?.invoke(this);
}
}
fun getUnderlyingComment(): IPlatformComment? {
return _commentLoaded;
}
fun setUIHandler(handler: (LazyComment)->Unit){
_uiHandler = handler;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
return _commentLoaded?.getReplies(client);
}
}
@@ -8,10 +8,12 @@ enum class ContentType(val value: Int) {
POST(2),
ARTICLE(3),
PLAYLIST(4),
WEB(7),
URL(9),
NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70),
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.contents
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime
interface IPlatformContent {
@@ -2,14 +2,17 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
interface IPlatformLiveEvent {
val type : LiveEventType;
var time: Long;
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -17,16 +18,21 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
val colorName: String?;
val badges: List<String>;
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
override var time: Long = -1;
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
this.name = name;
this.message = message;
this.thumbnail = thumbnail;
this.colorName = colorName;
this.badges = badges ?: listOf();
this.time = time;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
obj.ensureIsBusy();
val contextName = "LiveEventComment"
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
@@ -36,7 +42,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
obj.getOrThrow(config, "name", contextName),
obj.getOrThrow(config, "thumbnail", contextName, true),
obj.getOrThrow(config, "message", contextName),
colorName, badges);
colorName, badges,
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
}
}
}
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -20,6 +21,8 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
var expire: Int = 6000;
override var time: Long = -1;
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
this.name = name;
@@ -37,6 +40,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
obj.ensureIsBusy();
val contextName = "LiveEventDonation"
return LiveEventDonation(
obj.getOrThrow(config, "name", contextName),
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class LiveEventEmojis: IPlatformLiveEvent {
@@ -9,15 +10,17 @@ class LiveEventEmojis: IPlatformLiveEvent {
val emojis: HashMap<String, String>;
override var time: Long = -1;
constructor(emojis: HashMap<String, String>) {
this.emojis = emojis;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis"
return LiveEventEmojis(
obj.getOrThrow(config, "emojis", contextName));
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
}
}
}
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent {
@@ -10,20 +12,26 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String;
val targetThumbnail: String;
val targetUrl: String;
val isOutgoing: Boolean;
constructor(name: String, url: String, thumbnail: String) {
override var time: Long = -1;
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
this.targetName = name;
this.targetUrl = url;
this.targetThumbnail = thumbnail;
this.isOutgoing = isOutgoing;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
obj.ensureIsBusy();
val contextName = "LiveEventRaid"
return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName),
obj.getOrThrow(config, "targetThumbnail", contextName));
obj.getOrThrow(config, "targetThumbnail", contextName),
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
}
}
}
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class LiveEventViewCount: IPlatformLiveEvent {
@@ -9,12 +10,15 @@ class LiveEventViewCount: IPlatformLiveEvent {
val viewCount: Int;
override var time: Long = -1;
constructor(viewCount: Int) {
this.viewCount = viewCount;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
obj.ensureIsBusy();
val contextName = "LiveEventViewCount"
return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName));
@@ -5,7 +5,8 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
enum class TextType(val value: Int) {
RAW(0),
HTML(1),
MARKUP(2);
MARKUP(2),
CODE(3);
companion object {
fun fromInt(value: Int): TextType
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault
import com.futo.platformplayer.serializers.IRatingSerializer
@@ -13,8 +14,12 @@ interface IRating {
companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
obj?.ensureIsBusy();
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
};
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
obj.ensureIsBusy();
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
obj.ensureIsBusy();
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
}
}
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
}
}
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
}
}
@@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoMuxedSourceDescriptor(
class DownloadedVideoMuxedSourceDescriptor(
private val video: VideoLocal
) : VideoMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
@@ -2,10 +2,24 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor {
override val videoSources: Array<IVideoSource>;
override val audioSources: Array<IAudioSource>;
constructor(video: VideoLocal) {
videoSources = video.videoSource.toTypedArray();
audioSources = video.audioSource.toTypedArray();
}
constructor(audio: LocalAudioContentSource) {
videoSources = arrayOf()
audioSources = arrayOf(audio);
}
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
this.videoSources = videoSources;
this.audioSources = audioSources;
}
}
@@ -13,7 +13,9 @@ class AudioUrlSource(
override val codec: String = "",
override val language: String = Language.UNKNOWN,
override val duration: Long? = null,
override var priority: Boolean = false
override var priority: Boolean = false,
override var original: Boolean = false,
var isLocal: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null;
@@ -36,7 +38,9 @@ class AudioUrlSource(
source.container,
source.codec,
source.language,
source.duration
source.duration,
source.priority,
source.original
);
ret.streamMetaData = streamData;
@@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
override val language: String,
override val duration: Long?,
override val priority: Boolean,
override val original: Boolean,
val url: String
) : IAudioUrlSource {
override fun getAudioUrl(): String {
@@ -8,4 +8,5 @@ interface IAudioSource {
val language : String;
val duration : Long?;
val priority: Boolean;
val original: Boolean;
}
@@ -1,6 +1,3 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource {
val bearerToken: String
val licenseUri: String
}
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
@@ -0,0 +1,5 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IDashManifestWidevineSource : IWidevineSource {
val url: String
}
@@ -0,0 +1,3 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
@@ -0,0 +1,9 @@
package com.futo.platformplayer.api.media.models.streams.sources
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
interface IWidevineSource {
val licenseUri: String
val hasLicenseRequestExecutor: Boolean
fun getLicenseRequestExecutor(): JSRequestExecutor?
}
@@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
override val duration: Long? = null;
override var priority: Boolean = false;
override val original: Boolean = false;
val filePath : String;
val fileSize: Long;
@@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
}
companion object {
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
return LocalAudioSource(
source.name,
path,
fileSize,
source.bitrate,
source.container,
overrideContainer ?: source.container,
source.codec,
source.language
);
@@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
}
companion object {
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
return LocalVideoSource(
source.name,
path,
@@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
source.width,
source.height,
source.duration,
source.container,
overrideContainer ?: source.container,
source.codec,
source.bitrate?:0
);
@@ -14,7 +14,8 @@ open class VideoUrlSource(
override val codec : String = "",
override val bitrate : Int? = 0,
override var priority: Boolean = false
override var priority: Boolean = false,
var isLocal: Boolean = false
) : IVideoUrlSource, IStreamMetaDataSource {
override var streamMetaData: StreamMetaData? = null;
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import java.time.OffsetDateTime
/**
* A search result representing a video (overview data)
@@ -12,5 +13,10 @@ interface IPlatformVideo : IPlatformContent {
val duration: Long;
val viewCount: Long;
val playbackTime: Long;
val playbackDate: OffsetDateTime?;
val isLive : Boolean;
val isShort: Boolean;
}
@@ -0,0 +1,122 @@
package com.futo.platformplayer.api.media.models.video
import android.annotation.SuppressLint
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.core.net.toUri
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.states.StateApp
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
@kotlinx.serialization.Serializable
open class LocalVideoDetails(
override val id: PlatformID,
override val name: String,
override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink,
override val url: String,
override val duration: Long,
val mimeType: String? = null,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override val datetime: OffsetDateTime?
) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override val isLive: Boolean get() = false;
override val dash: IDashManifestSource? get() = null;
override val hls: IHLSManifestSource? get() = null;
override val live: IVideoSource? get() = null;
override val shareUrl: String = ""
override val viewCount: Long = -1
override val rating: IRating = RatingLikes(0)
override val description: String = "";
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
(LocalVideoUnMuxedSourceDescriptor(
arrayOf(),
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
))
else (LocalVideoMuxedSourceDescriptor(
LocalVideoContentSource(url, mimeType ?: "", name)
))
);
override val preview: ISerializedVideoSourceDescriptor? = null;
override val subtitles: List<SubtitleRawSource> = listOf()
override val isShort: Boolean = false
fun toJson() : String {
return Json.encodeToString(this);
}
fun fromJson(str : String) : SerializedPlatformVideoDetails {
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
companion object {
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
if(filePath.startsWith("content://"))
return fromContent(filePath, mimeType);
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
}
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
}
@SuppressLint("Range")
private fun getFileNameFromContentUrl(url: String): String? {
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
cursor?.moveToFirst();
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
cursor?.close();
return fileName;
}
}
}
@@ -3,33 +3,39 @@ package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime
@kotlinx.serialization.Serializable
open class SerializedPlatformVideo(
override val contentType: ContentType = ContentType.MEDIA,
override val id: PlatformID,
override val name: String,
override val thumbnails: Thumbnails,
override val thumbnails: Thumbnails = Thumbnails(),
override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override val datetime: OffsetDateTime?,
@JsonNames("datetime", "dateTime")
override val datetime: OffsetDateTime? = null,
override val url: String,
override val shareUrl: String,
override val shareUrl: String = "",
override val duration: Long,
override val viewCount: Long,
override val isShort: Boolean = false
) : IPlatformVideo, SerializedPlatformContent {
override val contentType: ContentType = ContentType.MEDIA;
override val isLive: Boolean = false;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override fun toJson() : String {
return Json.encodeToString(this);
}
@@ -43,6 +49,7 @@ open class SerializedPlatformVideo(
companion object {
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
return SerializedPlatformVideo(
ContentType.MEDIA,
video.id,
video.name,
video.thumbnails,
@@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
@@ -38,10 +37,15 @@ open class SerializedPlatformVideoDetails(
override val video: ISerializedVideoSourceDescriptor,
override val preview: ISerializedVideoSourceDescriptor?,
override val subtitles: List<SubtitleRawSource> = listOf()
override val subtitles: List<SubtitleRawSource> = listOf(),
override val isShort: Boolean = false
) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override val isLive: Boolean get() = false;
override val dash: IDashManifestSource? get() = null;
@@ -54,8 +54,12 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
}
override fun getCopy(privateCopy: Boolean): JSClient {
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
}
override fun initialize() {
@@ -10,6 +10,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@@ -22,6 +23,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
@@ -31,6 +33,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
@@ -41,6 +44,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
@@ -57,9 +61,13 @@ import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.Random
import kotlin.Exception
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction
@@ -81,6 +89,8 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null;
private var _usedReloadData: String? = null;
protected val _script: String;
private var _initialized: Boolean = false;
@@ -96,14 +106,14 @@ open class JSClient : IPlatformClient {
override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0;
val isBusy: Boolean get() = _plugin.isBusy;
val isBusyAction: String get() {
return _busyAction;
}
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>;
@@ -116,6 +126,7 @@ open class JSClient : IPlatformClient {
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
fun getSubscriptionRateLimit(): Int? {
val pluginRateLimit = config.subscriptionRateLimit;
@@ -193,8 +204,12 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
}
fun getUnderlyingPlugin(): V8Plugin {
@@ -208,12 +223,31 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id];
}
fun setReloadData(data: String?) {
if(data == null) {
if(declareOnEnable.containsKey("__reloadData"))
declareOnEnable.remove("__reloadData");
}
else
declareOnEnable.put("__reloadData", data ?: "");
}
fun getReloadData(orLast: Boolean): String? {
if(declareOnEnable.containsKey("__reloadData"))
return declareOnEnable["__reloadData"];
else if(orLast)
return _usedReloadData;
return null;
}
override fun initialize() {
Logger.i(TAG, "Plugin [${config.name}] initializing");
if (_initialized) return
plugin.start();
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true;
@@ -237,7 +271,9 @@ open class JSClient : IPlatformClient {
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
);
try {
@@ -252,19 +288,28 @@ open class JSClient : IPlatformClient {
}
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
fun enable() {
fun enable() = isBusyWith("enable") {
if(!_initialized)
initialize();
for(toDeclare in declareOnEnable) {
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
}
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
if(declareOnEnable.containsKey("__reloadData")) {
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
_usedReloadData = declareOnEnable["__reloadData"];
declareOnEnable.remove("__reloadData");
}
_enabled = true;
}
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
fun saveState(): String? {
fun saveState(): String? = isBusyWith("saveState") {
ensureEnabled();
if(!capabilities.hasSaveState)
return null;
return@isBusyWith null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return resp;
return@isBusyWith resp;
}
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@@ -287,6 +332,13 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getHome()"));
}
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
ensureEnabled()
return@isBusyWith JSVideoPager(config, this,
plugin.executeTyped("source.getShorts()"))
}
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
@@ -305,8 +357,10 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!;
}
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return _searchCapabilities!!;
return busy {
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return@busy _searchCapabilities!!;
}
}
catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex);
@@ -334,8 +388,10 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!;
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return _searchChannelContentsCapabilities!!;
return busy {
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return@busy _searchChannelContentsCapabilities!!;
}
}
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search")
@@ -360,17 +416,21 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
}
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
ensureEnabled();
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
}
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)")
override fun isChannelUrl(url: String): Boolean {
override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") {
try {
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex);
return false;
return@isBusyWith false;
}
}
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@@ -388,9 +448,10 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) {
return _channelCapabilities!!;
}
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return _channelCapabilities!!;
return busy {
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return@busy _channelCapabilities!!;
};
}
catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex);
@@ -501,14 +562,14 @@ open class JSClient : IPlatformClient {
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
@JSDocsParameter("url", "A content url (May not be your platform)")
override fun isContentDetailsUrl(url: String): Boolean {
override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") {
try {
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex);
return false;
return@isBusyWith false;
}
}
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@@ -540,7 +601,7 @@ open class JSClient : IPlatformClient {
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject)
return@isBusyWith JSPlaybackTracker(config, tracker);
return@isBusyWith JSPlaybackTracker(this, tracker);
else
return@isBusyWith null;
}
@@ -582,7 +643,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
@@ -610,17 +670,19 @@ open class JSClient : IPlatformClient {
@JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean {
override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
if (!capabilities.hasGetPlaylist)
return false;
return@isBusyWith false;
try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
return@isBusyWith busy {
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return false;
return@isBusyWith false;
}
}
@JSOptional
@@ -651,6 +713,13 @@ open class JSClient : IPlatformClient {
.toTypedArray();
}
@JSOptional
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
override fun getUserHistory(): IPager<IPlatformContent> {
ensureEnabled();
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
}
fun validate() {
try {
plugin.start();
@@ -722,19 +791,29 @@ open class JSClient : IPlatformClient {
return urls;
}
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try {
synchronized(_busyLock) {
_busyCounter++;
}
_busyAction = actionName;
return handle();
fun <T> busy(handle: ()->T): T {
return _plugin.busy {
return@busy handle();
}
finally {
_busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
}
fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
return _plugin.busy {
return@busy runBlocking {
return@runBlocking handle();
}
}
}
fun <T> isBusyWith(actionName: String, handle: ()->T): T {
//val busyId = kotlin.random.Random.nextInt(9999);
return busy {
try {
_busyAction = actionName;
return@busy handle();
}
finally {
_busyAction = "";
}
}
}
@@ -1,6 +1,10 @@
package com.futo.platformplayer.api.media.platforms.js
@kotlinx.serialization.Serializable
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.Dictionary
@Serializable
class SourcePluginAuthConfig(
val loginUrl: String,
val completionUrl: String? = null,
@@ -11,5 +15,44 @@ class SourcePluginAuthConfig(
val userAgent: String? = null,
val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null
) { }
val loginWarning: String? = null,
val loginWarnings: List<Warning>? = null,
val uiMods: List<UIMod>? = null
) {
@Serializable
class Warning(
val url: String,
val text: String?,
val details: String? = null,
val once: Boolean? = true
) {
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
@Serializable
class UIMod(
val url: String,
val scale: Float?,
val desktop: Boolean?
) {
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
}
@@ -4,6 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual
@@ -33,6 +34,7 @@ class SourcePluginConfig(
override val allowEval: Boolean = false,
override val allowUrls: List<String> = listOf(),
override val packages: List<String> = listOf(),
override val packagesOptional: List<String> = listOf(),
val settings: List<Setting> = listOf(),
@@ -46,11 +48,14 @@ class SourcePluginConfig(
var subscriptionRateLimit: Int? = null,
var enableInSearch: Boolean = true,
var enableInHome: Boolean = true,
var enableInShorts: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
var changelog: HashMap<String, List<String>>? = null
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -100,6 +105,10 @@ class SourcePluginConfig(
if(!packages.contains(pack))
return false;
}
for(pack in newConfig.packagesOptional) {
if(!packagesOptional.contains(pack))
return false;
}
//Developer Submit Url should be same or empty
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
return false;
@@ -128,7 +137,7 @@ class SourcePluginConfig(
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
if (currentlyInstalledPlugin != null) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
list.add(Pair(
"Different Author",
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
@@ -161,12 +170,17 @@ class SourcePluginConfig(
}
fun validate(text: String): Boolean {
if(scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present");
if(scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
try {
if (scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present");
if (scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
return false
}
}
fun isUrlAllowed(url: String): Boolean {
@@ -177,6 +191,19 @@ class SourcePluginConfig(
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
}
fun getChangelogString(version: String): String?{
if(changelog == null || !changelog!!.containsKey(version))
return null;
val changelog = changelog!![version]!!;
if(changelog.size > 1) {
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
}
else if(changelog.size == 1) {
return "Changelog (${version})\n" + changelog[0].trim();
}
return null;
}
companion object {
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
@@ -184,6 +211,8 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl;
return obj;
}
private val TAG = "SourcePluginConfig"
}
@kotlinx.serialization.Serializable
@@ -5,10 +5,16 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@Serializable
@@ -103,12 +109,22 @@ class SourcePluginDescriptor {
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
var enableHome: Boolean? = null;
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
var enableSearch: Boolean? = null;
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
var enableShorts: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync")
var sync = Sync();
@Serializable
class Sync {
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
var enableHistorySync: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
var rateLimit = RateLimit();
@Serializable
class RateLimit {
@@ -143,6 +159,8 @@ class SourcePluginDescriptor {
tabEnabled.enableHome = config.enableInHome
if(tabEnabled.enableSearch == null)
tabEnabled.enableSearch = config.enableInSearch
if(tabEnabled.enableShorts == null)
tabEnabled.enableShorts = config.enableInShorts
}
}
@@ -38,7 +38,7 @@ class JSHttpClient : ManagedHttpClient {
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
//Temporary ugly solution for DevPortal proxy support
(if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && StateDeveloper.instance.devProxy != null)
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
))
@@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient {
}
fun resetAuthCookies() {
_currentCookieMap.clear();
if(!_auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
if(!_captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _captcha!!.cookieMap!!) {
if(_currentCookieMap.containsKey(domainCookies.key))
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
else
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
}
}
fun clearOtherCookies() {
_otherCookieMap.clear();
}
override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
@@ -127,7 +146,7 @@ class JSHttpClient : ManagedHttpClient {
}
if(doApplyCookies) {
if (_currentCookieMap.isNotEmpty()) {
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) {
val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap
@@ -135,6 +154,12 @@ class JSHttpClient : ManagedHttpClient {
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
}
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
@@ -1,10 +1,12 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -12,6 +14,7 @@ interface IJSContent: IPlatformContent {
companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
obj.ensureIsBusy();
val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
@@ -26,6 +29,9 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj);
ContentType.ARTICLE -> JSArticle(config, obj);
ContentType.WEB -> JSWeb(config, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}
@@ -6,17 +6,20 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent {
companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
obj.ensureIsBusy();
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
ContentType.WEB -> JSWebDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}
@@ -0,0 +1,39 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
final override val contentType: ContentType get() = ContentType.ARTICLE;
override val summary: String;
override val thumbnails: Thumbnails?;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformArticle";
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
}
}

Some files were not shown because too many files have changed in this diff Show More