Compare commits

...

586 Commits

Author SHA1 Message Date
Kelvin af6d219936 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-08-30 18:39:17 +02:00
Kelvin 82a07e2e09 Refs 2024-08-30 18:39:11 +02:00
Koen J 12a9b99fff Fixed video being cut off. 2024-08-30 15:26:14 +02:00
Koen J 3adf761158 Fixed resource leak. 2024-08-30 13:44:14 +02:00
Koen J 670a4c61ff Changed reporting rates for sequential downloads. 2024-08-30 13:34:01 +02:00
Koen J 220f50d3bb Fixed quality selection when clicking download with HLS selected by default. 2024-08-30 13:25:47 +02:00
Koen J e0bf9d2a7c Fixed system bars hiding when not fullscreen. 2024-08-30 13:10:48 +02:00
Koen J f61cf46a52 Reverted landscape. 2024-08-30 12:06:52 +02:00
Koen J d188128d27 Added setting to allow video to go under cutout 2024-08-30 12:04:10 +02:00
Koen J f698c4120d Allow going under display cutout 2024-08-30 10:35:54 +02:00
Koen J 338a852d49 Updated to latest submodules. 2024-08-30 09:58:50 +02:00
Kelvin a64ee2242c Max download parallelism setting 2024-08-29 20:28:16 +02:00
Kelvin e9ff5e6f0b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-08-29 19:52:50 +02:00
Kelvin f3911d8b68 HLS Audio auto download fix, httpClient setTimeout support 2024-08-29 19:52:40 +02:00
Koen J 9ce0be6450 Adjusted timeouts 2024-08-29 18:30:10 +02:00
Koen 6ab3eff61c Merge branch 'download-fixes' into 'master'
Download fixes

See merge request videostreaming/grayjay!33
2024-08-29 16:07:20 +00:00
Koen J 0281da1c5a Fixed progress for sequential downloads. 2024-08-29 18:06:53 +02:00
Koen J 0b4770188c Better error 2024-08-29 17:57:55 +02:00
Koen J 9376bb05fa Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into download-fixes 2024-08-29 15:45:05 +02:00
Kelvin ecca3b6793 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-08-29 15:43:44 +02:00
Kelvin f41a971cd8 Fix download states 2024-08-29 15:43:29 +02:00
Koen 44ba66d619 Merge branch 'download-fixes' into 'master'
Download fixes.

See merge request videostreaming/grayjay!32
2024-08-29 13:42:18 +00:00
Koen bf685a607f Download fixes. 2024-08-29 13:42:18 +00:00
Koen J 5713cf0508 Processed feedback. 2024-08-29 15:41:53 +02:00
Koen J bdd50d70ca Download fixes. 2024-08-29 13:19:24 +02:00
Koen 8188399ce6 Merge branch 'new-plugins' into 'master'
feat:  add bichute and dailymotion to embedded sources

See merge request videostreaming/grayjay!31
2024-08-29 10:02:00 +00:00
Stefan f72b7dbbbb feat: add bichute and dailymotion to embedded sources 2024-08-29 09:59:54 +01:00
Kelvin 2409afcc5c Maxheight on code view 2024-08-28 21:26:16 +02:00
Kelvin 15c0d02c13 Use harbor links instead of polycentric 2024-08-28 21:21:00 +02:00
Kelvin a54a5081e6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-08-28 20:37:03 +02:00
Kelvin db9dfcf049 Working downloads for DashManifestRaw sources with RequestExecutors 2024-08-28 20:36:50 +02:00
Koen J 47f9948748 Initial fix for UMP casting. 2024-08-28 15:54:15 +02:00
Kai DeLorenzo 05e866df55 Merge branch 'landscape' into 'master'
Landscape

See merge request videostreaming/grayjay!29
2024-08-27 11:03:04 +00:00
Kai DeLorenzo fc431f0cb8 Merge branch 'update-deps' into 'master'
Update deps

See merge request videostreaming/grayjay!30
2024-08-27 10:58:13 +00:00
Kai DeLorenzo 228ab359ed Merge branch 'refs/heads/master' into update-deps
# Conflicts:
#	dep/futopay
#	dep/polycentricandroid
2024-08-27 05:54:35 -05:00
Kai DeLorenzo 103a8587f7 update commit hash reference 2024-08-27 05:47:36 -05:00
Koen 7db0083928 Merge branch 'mdns' into 'master'
Custom mDNS implementation for faster discovery.

See merge request videostreaming/grayjay!28
2024-08-26 13:29:57 +00:00
Koen e6f6ab499a Custom mDNS implementation for faster discovery. 2024-08-26 13:29:57 +00:00
Kelvin 721b7dbba0 Better source swapping for lazily generated sources 2024-08-22 22:47:47 +02:00
Kelvin a95ddab814 Merge 2024-08-22 21:01:42 +02:00
Kelvin 2941546ae4 DashManifestRaw support, RequestExecutor support, http binary body and response support, spec version support, ignore unsupported sources, webm container preference in settings 2024-08-22 21:00:06 +02:00
Kai DeLorenzo bd9b9179c1 rename variable 2024-08-22 12:50:13 -05:00
Kai DeLorenzo ce7d54c151 fixed rotation issues 2024-08-22 12:43:39 -05:00
Kai DeLorenzo 3c778c07c2 removed gap at the top where the notifications show 2024-08-21 18:20:24 -05:00
Kai 95207341db bottom bar and tutorial fixes 2024-08-21 15:50:22 -05:00
Kai 70cf24924d initial gridlayout 2024-08-21 14:33:26 -05:00
Kai a8ebba691e update gradle version 2024-08-21 14:22:54 -05:00
Koen J ec19ea44ad Implemented fix for media session vanishing after 10 minutes. 2024-08-21 16:05:37 +02:00
Koen J ca8dc0f0f5 Auto rotate fixes. 2024-08-20 16:24:47 +02:00
Koen J 1dc50a697c Hybrid orientation approach. 2024-08-20 15:44:57 +02:00
Koen J 1167c314ee Intermediate commit point 2024-08-20 14:25:07 +02:00
Kelvin 55781e2b34 Download estimations, codec in sources, wip plugin source request executor, setting to disable source deduplication (simplify sources), support for SlideUpItem descriptions, bigger SlideUpItems 2024-08-13 20:47:16 +02:00
Kelvin 7439e44e44 SLD checks, minor fixes 2024-08-13 14:56:36 +02:00
Koen J cf2639df3d Build fixes. 2024-08-06 12:19:58 +02:00
Koen 834de928c2 Rotation fixes. 2024-08-05 13:30:45 +02:00
Kelvin 72efb21439 Mark as watched action 2024-07-17 21:12:24 +02:00
Kelvin aa8790ebdb Remove mediasession interval 2024-07-17 20:11:56 +02:00
Kelvin 6d491052ee Invert privatemode boolean 2024-07-17 19:56:38 +02:00
Kelvin 87ff4691ce Merge branch 'playback-experiment' into 'master'
403 Bypass & Privacy mode

See merge request videostreaming/grayjay!26
2024-07-17 17:32:54 +00:00
Kelvin 34d76e79ed Mandatory host body and suffic for wildcard urls 2024-07-17 19:31:59 +02:00
Kelvin 31b43da96f Pass private client pool variable 2024-07-17 18:30:30 +02:00
Kelvin 0540e673e2 Remove under construction on sources 2024-07-17 18:24:59 +02:00
Kelvin 4e88a63809 Privacy mode, Handle 403s 2024-07-17 18:11:08 +02:00
Kelvin f7581f8a65 Block bypass attempts 2024-07-17 13:58:00 +02:00
Kelvin e87a1c079c Experimentation code 2024-07-17 01:37:53 +02:00
Kelvin 3f9477c246 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-16 20:18:54 +02:00
Kelvin 05ed1e188e Logging and refs 2024-07-16 20:18:46 +02:00
Koen f3d06e49f8 Added setting to always proxy requests for FCast. Added logging to print dash manifests. 2024-07-15 10:16:54 +02:00
Koen f9a4b68967 Updated submodules. 2024-07-14 15:39:57 +02:00
Kai DeLorenzo 3631cfe365 fix unstable api error 2024-07-05 12:06:32 -05:00
Koen 8766ae176e Updated Spotify and possible fix for media session being killed after 10 minutes of inactivity. 2024-07-04 10:49:59 +02:00
Kelvin 36b53d490f Exclude HLS and Dash from source dedup 2024-07-03 21:37:15 +02:00
Kelvin f9b8b812a4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-03 20:51:02 +02:00
Kelvin ac9eae5272 refs 2024-07-03 20:50:52 +02:00
Tom f270cc00d8 Merge branch 'github-issue-templates' into 'master'
Add issue template for bugs, docs, feature requests

See merge request videostreaming/grayjay!22
2024-07-02 20:41:49 +00:00
Kelvin a5a3f970da Merge 2024-06-30 18:03:29 +02:00
Kelvin 987c465bf8 Refs 2024-06-30 18:01:41 +02:00
Kai DeLorenzo cf3c766fd9 Update documentation_issue.yml 2024-06-28 15:34:55 +00:00
Kai DeLorenzo 7efafae432 Update feature_request.yml 2024-06-28 15:30:53 +00:00
Kai DeLorenzo 1b8f44dde3 Update bug_report.yml 2024-06-28 15:24:37 +00:00
Thomas Folbrecht 4d93246863 spelling 2024-06-27 14:53:27 -05:00
Thomas Folbrecht 0471886d9f forgot to commit changes to config 2024-06-27 14:31:52 -05:00
Thomas Folbrecht 266974b799 forgot to commit changes to config 2024-06-27 14:30:42 -05:00
Thomas Folbrecht c3663c67d7 add labeler, fix copy 2024-06-27 12:59:03 -05:00
Thomas Folbrecht 07bb23d10b fix license contact link 2024-06-26 20:04:32 -05:00
Thomas Folbrecht 749fc22c6b rm contact 2024-06-26 15:59:46 -05:00
Thomas Folbrecht 9f9a4e8298 Add issue template for bugs, docs, feature requests
points users to chat.futo.org for support
2024-06-26 15:42:12 -05:00
Kai DeLorenzo 39e7d64d3f remove save icon after saving 2024-06-26 15:03:01 -05:00
Kai DeLorenzo 35d8610c00 Update packageHttp.md 2024-06-26 17:01:25 +00:00
Koen bc550ae8f5 Removed exporting service. 2024-06-26 16:01:08 +02:00
Kai DeLorenzo c76ef7f19b Merge branch 'playlist-fixes' into 'master'
Playlist Fixes

See merge request videostreaming/grayjay!21
2024-06-25 15:35:46 +00:00
Kai DeLorenzo b7781264d3 changed playlist limit to 100
added save button to non-saved local playlists
2024-06-25 10:22:23 -05:00
Kai DeLorenzo 696e03941a pass through actions to local playlist and auto convert playlists with 20 or fewer videos 2024-06-24 13:00:58 -05:00
Kai DeLorenzo 4609a351dc don't save playlists that weren't explicitly copied
fixed exception failed to convert playlist job cancelled
2024-06-24 10:50:40 -05:00
Kelvin K c275415a49 Hide playlist video count if unknown 2024-06-20 11:51:11 +02:00
Kelvin K 486ebd6bc8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-19 19:14:20 +02:00
Kelvin K 74b9926647 Refs 2024-06-19 19:14:05 +02:00
Koen 2a6ba6d541 Fixed remote playlist ToPlaylist. 2024-06-14 14:54:37 +02:00
Koen 931216ab7d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-14 13:32:10 +02:00
Koen 916936e179 Implemented proper remote playlist support. 2024-06-14 13:32:00 +02:00
Kelvin K b535353365 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-14 13:23:10 +02:00
Kelvin K be2ae096ee Fix locked content deserializer 2024-06-14 13:22:58 +02:00
Koen 948b85ddcb Pushed updated submodules. 2024-06-14 08:43:18 +02:00
Kelvin K ae904b4cd8 Content recommendation api support, removing old CachedPlatformClient 2024-06-13 17:46:22 +02:00
Kelvin K aad50e7b50 Improved playlist import support 2024-06-13 13:45:31 +02:00
Kelvin K ff28a07871 Fix loop offline videos, make loop not reload video 2024-06-13 11:21:48 +02:00
Kelvin K 414b6e24d2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-10 12:57:28 +02:00
Kelvin K 9499afd815 Twitch refs 2024-06-10 12:57:17 +02:00
Koen e7aca5cd25 Merge branch 'ian-master-patch-14410' into 'master'
Update LICENSE.md

See merge request videostreaming/grayjay!20
2024-06-08 06:50:24 +00:00
Ian Mason 80a6a8ac9f Update LICENSE.md 2024-06-07 23:34:03 +00:00
Kelvin c3428a695f Merge branch 'channel-playlists-ui' into 'master'
add support for channel playlists on the channel page

See merge request videostreaming/grayjay!18
2024-06-07 15:20:20 +00:00
Kelvin 1a9665b5c6 Merge branch 'disable-spotify-download' into 'master'
disable download button for widevine sources

See merge request videostreaming/grayjay!19
2024-06-07 15:19:25 +00:00
Kai DeLorenzo ebb4693425 adjust tab order 2024-06-07 09:53:19 -05:00
Kelvin K 4f09f48ace Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-07 12:52:36 +02:00
Kelvin K a0d6ff912b App version info for plugins, trust all certs dev setting, latest refs 2024-06-07 12:52:25 +02:00
Koen a345da0feb Added spotify plugin. Fixed bilibili signing. Added bilibili and spotify link handling. 2024-06-07 09:32:16 +02:00
Kai DeLorenzo fc5a8d9531 disable download button for widevine sources 2024-06-06 17:33:35 -05:00
Koen 7353edb058 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-06 20:07:18 +02:00
Koen 2a7c0a5c79 Changed share intent. 2024-06-06 20:07:13 +02:00
Kai DeLorenzo 4cf3aabe89 removed additional hardcoding 2024-06-05 18:57:43 -05:00
Kai DeLorenzo ef284ba51d fixed tab changing when adding the playlist tab 2024-06-05 13:44:05 -05:00
Kai DeLorenzo 5edd389e84 removed hardcoding. fixed bugs. hide CHANNELS and SUPPORT for non polycentric linked channels 2024-06-04 20:22:42 -05:00
Koen 309332ac9c Update LICENSE 2024-06-03 16:30:27 +00:00
Koen 035d19f581 Update LICENSE 2024-06-03 16:30:05 +00:00
Kelvin 72bb43f934 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-05-31 21:59:17 +02:00
Kelvin 447ed6bf21 Live chat interval removel, non-self return http calls to prevent crash, minor doc fix, more logs 2024-05-31 21:59:07 +02:00
Koen db1bcfcc6b Allow user certificates to do full network request proxying. 2024-05-31 11:25:34 +02:00
Kai DeLorenzo 1ccae84933 add support for channel playlists on the channel page 2024-05-28 17:05:35 -05:00
Kelvin 152b9b23cd Intercept non-implemented getChannelPlaylists 2024-05-27 01:15:37 +02:00
Kelvin a3070d8d08 getChannelPlaylist support 2024-05-27 01:13:32 +02:00
Kelvin aceab7b476 Websocket fixes, onConcluded support 2024-05-21 22:31:04 +02:00
Kelvin 5f1c0209a8 Additional risk check 2024-05-20 22:38:18 +02:00
Kelvin 819e81b7a6 Proxy support, Additional http header access support 2024-05-20 22:28:51 +02:00
Kelvin 8193234c2f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-05-20 15:45:17 +02:00
Kelvin 6263a31f41 Minor devportal improvements 2024-05-20 15:44:43 +02:00
Kelvin 481a0cda99 Merge branch 'drm' into 'master'
add initial widevine drm support for audio url sources

See merge request videostreaming/grayjay!16
2024-05-20 13:33:59 +00:00
Kelvin b39b89e908 Make type constant public 2024-05-20 13:33:06 +00:00
Kai DeLorenzo ce0f98055f added initial drm support for audio url sources 2024-05-17 18:45:44 -04:00
Koen 3dddf68766 Fully swap over to prod url. 2024-05-17 12:11:02 +02:00
Kelvin 88d687f26e Update trigger on exception update button pressed 2024-05-16 22:27:53 +02:00
Kelvin d44df42727 Plugin auto-update support and prompting 2024-05-15 21:26:44 +02:00
Kai DeLorenzo 88c8dbcb7c added initial drm support for audio url sources 2024-04-29 13:58:00 -05:00
Kelvin b4fddbe26a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-04-24 20:13:40 +02:00
Kelvin ab6d7669d7 Delete dangling exports 2024-04-24 20:13:32 +02:00
Koen 3f22c7f717 Added polycentric user agent. 2024-04-24 16:02:26 +02:00
Kelvin f36e9588cb Use proper calls for thumbnail 2024-04-23 21:15:18 +02:00
Kelvin 8f99f399ee Refs and tests 2024-04-23 19:26:30 +02:00
Kelvin 56166a7948 Support for chinese, japanese, arabic file names for export 2024-04-23 19:24:59 +02:00
Kelvin 4edd8ee1ea Fix crash on extreme pip aspect ratios 2024-04-23 17:31:10 +02:00
Koen a830c918ab Finished embedding bilibili. 2024-04-23 15:07:10 +02:00
Kelvin 53f74c4b6e Fix for hanging app if logging is enabled 2024-04-22 19:58:22 +02:00
Kelvin 959c192762 Fix channel content not showing older non-videos, fix seperated channel contents not being fetched if not both streams and videos are included 2024-04-19 22:40:13 +02:00
Kelvin 8be7b1272b PeekChannelContent and initial algorithm support, DevSubmit support, Prevent crash url search before init, Documentation url scanning for devportal, limit ongoing downloads ui to 4 2024-04-19 20:16:09 +02:00
Kelvin 6b57878275 Fix post detail loader not disappearing 2024-04-16 21:15:47 +02:00
Kelvin 66c7741c38 Deleting playlist video deletes local files, Post links are now clickable, going back to channel page from post now shows channel page correctly, search capabilities correct for channel content search, Fix loader not disappearing in certain cases on post details 2024-04-16 21:15:28 +02:00
Kelvin b370af9d91 Grayjay logo, WatchLater button, WatchLater download, Download notification dismiss fix, Polycentric open platform, Minor utility additions, Dev method documentation url support 2024-04-12 23:39:33 +02:00
Kelvin 40b86cb5de Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-03-19 20:41:56 +01:00
Kelvin 84622e22aa Logo replacement 2024-03-19 20:41:01 +01:00
Koen 092b20041e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-03-08 08:17:51 +01:00
Koen f6cc00f471 Casting. 2024-03-08 08:17:38 +01:00
Kelvin be2067067b Year rounding 2024-03-06 21:59:55 +01:00
Kelvin 67a7dd9698 Refs 2024-03-06 21:44:48 +01:00
Kelvin 6ffc067b24 Support for cache in reconstructions, non-required cache added to exports, playlists shares now add a cache aswell for quicker importing 2024-03-06 21:39:30 +01:00
Kelvin 56e6314c11 Ref 2024-03-05 17:15:36 +01:00
Kelvin e590bb4a19 Fix 0 year issue 2024-03-05 00:05:46 +01:00
Kelvin 35fe7f0e7a Add type to unknown content exception 2024-03-01 15:31:30 +01:00
Koen 45d818ac81 Reverted dependencies. 2024-02-16 15:51:59 +01:00
Kelvin 7729681829 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-02-16 14:58:28 +01:00
Kelvin b12d04b27d Attempted fix for double controls 2024-02-16 14:58:17 +01:00
Koen e6608b9a5c Updated PolycentricAndroid. 2024-02-16 14:07:27 +01:00
Koen 2d503dfaf6 Added scroll to top. Full scrollable parent comment and Polycentric process secret backup and automatic database recovery. 2024-02-16 13:56:14 +01:00
Kelvin 08934ef8de Modify subscription groups in sub settings 2024-02-14 23:25:58 +01:00
Kelvin 62d927739a Sharing from overview options, notification channel names 2024-02-14 20:15:12 +01:00
Kelvin c8db8f58e8 Refs 2024-02-14 19:19:24 +01:00
Kelvin 0fc966a77d Subscription watched filter 2024-02-14 19:18:35 +01:00
Kelvin 9f6c6c8cf3 Fix support, fix membership urls 2024-01-23 23:51:21 +01:00
Kelvin 43a6ff138c Fix queue looping 2024-01-22 20:54:40 +01:00
Kelvin 269a3460e7 Fix live stream retrying 2024-01-22 15:52:51 +01:00
Koen 18150e9e15 Fixed bottom menu button visibility. 2024-01-19 20:29:00 +01:00
Koen 362c7f5b2c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-19 20:24:35 +01:00
Koen 2adb8ad7f9 Fixed brightness not working. 2024-01-19 20:24:23 +01:00
Kelvin 6b5d4e7507 Fix nullable 2024-01-19 19:44:52 +01:00
Kelvin 49c82726f0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-19 17:28:45 +01:00
Kelvin c8ddcda384 Refs, Dev portal improvements and on-device testing, Fix crashes on disabling v8 race conditions, edgecase where history could be null, issue on starting Grayjay with an url 2024-01-19 17:28:35 +01:00
Koen b75217f789 Possible fix for AudioNoisyReceiver popping up 'App is not responding'. 2024-01-19 17:02:24 +01:00
Koen 8ba8e535bd Added check for updates button on exception activity. 2024-01-19 16:13:42 +01:00
Koen e4c574db6b Fixed crash in updateAllButtonVisibility. 2024-01-19 15:38:22 +01:00
Koen fae73293d7 Fixed crash where it would fail to unregister audio noisy receiver. Fixed crash where system brightness setting does not exist. 2024-01-19 15:17:11 +01:00
Koen 3bd0aac4f8 Implemented system brightness in an alternative way. 2024-01-19 14:09:56 +01:00
Kelvin 26b822e04b Text edit 2024-01-17 17:23:38 +01:00
Kelvin 96b9b8843c Fix wrong visibility for no sources ui 2024-01-17 16:57:35 +01:00
Kelvin 6d9c1e17b5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 15:31:11 +01:00
Kelvin 507ad105c0 Hide warnings if empty, enable newly installed plugins, new browse plugin url 2024-01-17 15:31:05 +01:00
Koen 40a283017e Fixed issue where adding new playlist would require two back swipes to minimize video. 2024-01-17 15:26:09 +01:00
Kelvin be14597670 Merge 2024-01-17 13:28:54 +01:00
Kelvin 837609abb9 Remove primary client, remove play store default source, add additional flows for adding sources 2024-01-17 13:26:17 +01:00
Koen d64cd98b43 Removed most announcements. 2024-01-17 13:08:25 +01:00
Koen 0081ff1483 Removed playstore pre-installed PeerTube. 2024-01-17 12:54:45 +01:00
Kelvin f78ca6c7ed Toggle to disable update check for individual sources 2024-01-17 12:34:58 +01:00
Kelvin cfc7cbcaa4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 12:15:26 +01:00
Kelvin e533eb7778 Video zoom increase tolerances 2024-01-17 12:15:17 +01:00
Koen 7c1d0a7f88 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 11:02:13 +01:00
Koen 01ef471708 Better handling of null or empty id/url for Polycentric comments/likes. 2024-01-17 11:02:03 +01:00
Kelvin 2fd0a9a41d Fix scroll downloaded playlists 2024-01-16 22:31:45 +01:00
Kelvin 635749dfe4 Open channel/playlist urls through search 2024-01-16 21:42:43 +01:00
Kelvin c4bd5626f3 Fix usable motionlayout on portrait fullscreen 2024-01-16 21:21:26 +01:00
Kelvin 568a0f6329 Catch any failed auth/captcha decodes 2024-01-16 21:11:11 +01:00
Kelvin 7ee67b5cd0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 20:11:27 +01:00
Kelvin fc94c6903c Additional warnings for reinstall, disable uninstall if doesn't make sense, reduce maxheight to 40% of videoview 2024-01-16 20:11:23 +01:00
Koen a0af8805e7 Removed accidentally cmomitted code. 2024-01-16 20:09:02 +01:00
Koen 9b64cde17d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 20:08:15 +01:00
Koen f6931bcf8c Added isUserGesturing boolean to gesture control view. 2024-01-16 20:07:26 +01:00
Kelvin a4ff47d863 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 16:46:40 +01:00
Kelvin 982d251126 Prevent video reload if same video, refs 2024-01-16 16:46:30 +01:00
Koen 8820a0ecc0 Fixed slide subscription overlay not closing on back gesture in video detail view. Fixed bottom margin for minimized view progress bar. 2024-01-16 13:25:12 +01:00
Koen b99a713ffc Added 'Add to new playlist' to add button when watching video. 2024-01-16 13:04:44 +01:00
Koen dfc8c4b740 Added snapping when only panning. 2024-01-16 12:24:07 +01:00
Koen c3df9e5259 Changed tolerances on zooming/panning snapping. 2024-01-16 12:20:04 +01:00
Koen b9c7e0a8ca Made snapping only work when panned close to center. 2024-01-16 12:05:04 +01:00
Koen 2c7f02a24d Added zoom pan snapping. 2024-01-16 11:01:00 +01:00
Koen 5cc8488d94 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-15 18:07:08 +01:00
Koen 6f7304f59c Added ability to click on a comment to view where the comment was placed and the ability to navigate upwards in the replies overlay by clicking the parent comment. 2024-01-15 18:06:57 +01:00
Kelvin ea4fea4401 Deduplication priorities fixed, playpause button change and wakelock on interruption fixed 2024-01-13 00:51:00 +01:00
Kelvin 9b48664de4 Resolving wakelock on play interuptions 2024-01-12 23:22:49 +01:00
Kelvin 8964dc68f0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-12 21:34:49 +01:00
Kelvin 4711b8055b Empty home and install plugin flows. BrowserFragment optional url handling 2024-01-12 21:34:39 +01:00
Koen 84e3373fa7 Added settings to enable/disable zoom/pan gestures. 2024-01-11 15:48:58 +01:00
Koen fdd7e32dd8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-11 12:59:18 +01:00
Koen e57119ebbd Added setting for restore system brightness and finished zoom pan controls. 2024-01-11 12:59:10 +01:00
Kelvin ed29dd8365 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-10 17:53:52 +01:00
Kelvin 196cacb452 Open playlist urls support, indexOutOfBound fix for queue when deleting items 2024-01-10 17:53:45 +01:00
Koen c025913fc8 Fixed tutorial video aspect ratio. 2024-01-10 13:13:25 +01:00
Koen 48b2c68e72 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-10 13:07:33 +01:00
Koen 689766a6ac Added monetization tutorial and added tutorial descriptions. 2024-01-10 13:07:22 +01:00
Kelvin 9306024d17 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-09 22:25:49 +01:00
Kelvin 195163840b DOMParser toNodeTree, Better subscription errors, Watch later ordering 2024-01-09 22:25:41 +01:00
Koen 788c54bf8f Fixed durations for castview. 2024-01-09 13:50:51 +01:00
Koen 031aabd523 Proper implementation of editor action. 2024-01-09 13:37:56 +01:00
Koen 85db4cc4e6 Theoretical fix for double controls. 2024-01-08 14:19:21 +01:00
Koen 745aad385b Gesture controls can individually be enabled/disabled and can be toggled to use system or non-system values. 2024-01-08 13:56:11 +01:00
Koen ba87261f9f Added settings to enable/disable gestures. 2024-01-08 12:30:04 +01:00
Koen 7d091382c0 Do not restart threads when started is false. 2024-01-07 21:56:56 +01:00
Koen 781d0797e7 More casting fixes. 2024-01-07 18:18:15 +01:00
Koen ec12a06b88 Reverted disconnect based on pong timer. 2024-01-07 17:05:35 +01:00
Koen bf3e8867c3 Synchronized writes. 2024-01-07 14:19:03 +01:00
Koen 176814a715 Crashfix on unreliable casting connection. Made casting more robust with intermittent TCP connections. 2024-01-07 13:41:43 +01:00
Koen 898637a616 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-06 23:07:50 +01:00
Koen f1860126a7 - Changed casting connection timeout to 2 seconds.
- Added ping pong to FCast.
- Removal of DataInputStream and just using raw InputStream.
- Fix slider position crash.
2024-01-06 23:07:34 +01:00
Kelvin f8402676d7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-06 22:19:38 +01:00
Kelvin cf86ce1ab3 Minor leak fix, login warning support, refs 2024-01-06 22:19:29 +01:00
Koen f4cb1719e0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-04 13:38:37 +01:00
Koen 4898cb53ae Fixed tint color. 2024-01-04 13:38:30 +01:00
Kelvin 0f60d4737e Plugin update appToast, Refs 2024-01-03 19:47:38 +01:00
Kelvin 0dc33e1f2b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-03 17:59:11 +01:00
Kelvin d86a997a88 appToast system, VideoToOpen changed 2024-01-03 17:59:01 +01:00
Koen 34d4d92289 Casting stability fixes to ChromeCast connection thread. 2024-01-03 11:24:55 +01:00
Koen 4cb1bf268f Updated YouTube submodule. 2024-01-03 11:09:57 +01:00
Kelvin 8488706ff9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 20:14:36 +01:00
Kelvin a348bb2662 Fix crash download livestream, refs 2024-01-02 20:14:28 +01:00
Koen 60a17b3c67 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 19:48:51 +01:00
Koen 386c58d4ad Added timeouts to casting flow. 2024-01-02 19:48:43 +01:00
Kelvin 356ba01dc1 Fix default comment section 2024-01-02 16:28:08 +01:00
Kelvin ed2aa848da Fix clear playback notification, Update V8 Library, Close modified requests after usage 2024-01-02 15:55:29 +01:00
Kelvin c5dd90048f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-02 14:36:46 +01:00
Kelvin ab04f334dc Download cleanup after cancel/failure and on startup, auto-select subtitles if downloaded, refs 2024-01-02 14:36:41 +01:00
Koen 0d44f8a416 Fixed issue where some videos would not play in Odysee. 2024-01-02 13:36:43 +01:00
Koen d01a1545e2 Allow large heap to fix Rumble failing to load large videos. 2024-01-02 13:35:48 +01:00
Kelvin e599729ba1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-27 15:21:52 +01:00
Kelvin 3ac043740e Fix RequestModifier not being applied, and add default option to add pre-existing headers 2023-12-27 15:21:43 +01:00
Koen 89603d0ff3 changed typeface when update is available. 2023-12-27 14:31:26 +01:00
Koen 05b6cd7c97 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-27 11:11:36 +01:00
Koen ea5aad0631 Implemented check for updates. 2023-12-27 11:11:29 +01:00
Kelvin 96e034b9bf Fix plugin.d.ts and update 2023-12-22 19:29:38 +01:00
Kelvin 6141c36855 Refs 2023-12-21 20:04:53 +01:00
Kelvin 4084ab3ed0 refs 2023-12-21 20:01:51 +01:00
Kelvin 34e733823a Refs 2023-12-21 19:27:13 +01:00
Kelvin f1d01642cd Refs, ui fixes 2023-12-21 19:24:30 +01:00
Kelvin d5551d7118 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:24:18 +01:00
Kelvin d079a1e8e4 Add missing channel name setter 2023-12-21 17:24:11 +01:00
Koen c06c00ee9b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:19:17 +01:00
Koen 1d8eababc2 Minor casting dialog fixes. 2023-12-21 17:19:05 +01:00
Kelvin 75cf1ffbdd Offline playback fix with remote sources 2023-12-21 17:13:43 +01:00
Kelvin 5499706a9b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 16:14:01 +01:00
Kelvin ba57e32920 Minor queue styling, refs 2023-12-21 16:13:53 +01:00
Koen df96c5b51c Fixed live video previews in preview layout. Fixed time bar spacing at bottom. 2023-12-21 16:07:03 +01:00
Koen 75f81d20db Fixed buttons in subscription groups and made select button click only work when there are things selected. 2023-12-21 13:10:38 +01:00
Koen 3fc92e4065 Fixed rounding of subscription groups. 2023-12-21 12:56:35 +01:00
Koen 8ffd5f411f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 11:21:34 +01:00
Koen 918161a299 Fixed margins for subscription groups and menu bar icons now show filled variant if existing. 2023-12-21 11:20:53 +01:00
Kelvin 9f50f72eaa Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 11:12:45 +01:00
Kelvin 2f66f124aa Toast on cancel creation, default subgroup creator image, fix subgroup cache load 2023-12-21 11:12:34 +01:00
Koen 9a11717cf4 Fixed Rumble channel contents having empty author. Changed TutorialVideo so author is not clickable. 2023-12-21 10:20:52 +01:00
Kelvin 0d80424799 Optimized selection creator overlay, global subscription progress listener 2023-12-20 21:54:39 +01:00
Kelvin ed9a65b2f0 Filter cache results by enabled clients 2023-12-20 18:35:13 +01:00
Kelvin 8a53297be2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 18:08:52 +01:00
Kelvin 20862a27c8 Search creator overlay, Fix crash Add topbar 2023-12-20 18:08:40 +01:00
Koen 95785e6c78 Added something more similar to Jdenticons. 2023-12-20 17:35:57 +01:00
Koen e88c649578 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 17:09:19 +01:00
Koen 09f91e64fb Fixed Kick subscription imports. 2023-12-20 17:04:10 +01:00
Koen b8923e59a1 Fixed bottom bar new tabs not showing up for people who changed tabs. 2023-12-20 17:03:48 +01:00
Kelvin e722c0ce9a Explore subscription groups button 2023-12-20 17:02:40 +01:00
Kelvin 56248bf4b0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 12:36:12 +01:00
Kelvin 5af4787c45 Fix nested overlays in more buttons 2023-12-20 12:35:40 +01:00
Koen 0990247322 Fixed Rumble channel content fetching, channel image and show comments when sign in is not required. 2023-12-20 12:06:21 +01:00
Koen 0154525578 Revert "Fixed download button overlay not working in more button."
This reverts commit 1dc6eee242.
2023-12-20 12:02:35 +01:00
Koen 1dc6eee242 Fixed download button overlay not working in more button. 2023-12-20 11:51:28 +01:00
Koen c63a63cb33 Fixed Gesture control distances for portrait full screen. 2023-12-20 11:06:18 +01:00
Koen c1967556ac Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 10:56:28 +01:00
Koen 309a57f5a1 Added icon based on the system key when a polycentric user has not set a profile picture. Made managing Polycentric profile most robust. Fixed an issue where latest events were not always being shown in relation to Polycentric profiles. Added copyable (on long press) system key in Polycentric profile activity. Added tutorial videos tab and flow. 2023-12-20 10:56:16 +01:00
Kelvin ee0bc96e53 Support for initial skip chapters 2023-12-20 00:28:21 +01:00
Kelvin a4422fdd56 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-19 23:08:48 +01:00
Kelvin b7c4047f1d Progress bars subscription groups, Various notification permission requests, improved notification experience, Settings activity open to specific settings, refs 2023-12-19 23:08:38 +01:00
Koen 65174ffc97 Show connected controls as disabled when still connecting. 2023-12-19 11:46:40 +01:00
Koen eac3e37af5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-19 11:38:32 +01:00
Koen 0d5ad90ff9 Fixed duration format. Added tutorial fragment. Added dialog that asks you if you want to see the tutorials on the first app boot. Made casting when clicking start now open connected dialog. Fixed crashes related to sliders in casting connected control. Fixed history tab title. Removed old FCast encryption. 2023-12-19 11:38:22 +01:00
Kelvin f42b14e95a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-18 23:37:27 +01:00
Kelvin b8acd0b5b2 Subscriptions fetching support for subgroups 2023-12-18 23:37:10 +01:00
Koen ef72561768 Added support for chapter skip in casting. 2023-12-18 16:28:35 +01:00
Koen d63627bd61 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-18 15:48:17 +01:00
Koen 422cceb225 Added support for FCast encrypted connection upgrade and added support for ensuring threads are restarted onResume for FCast and ChromeCAst. 2023-12-18 15:48:05 +01:00
Kelvin 76f5962232 Temp remove niconico 2023-12-15 22:47:04 +01:00
Kelvin 30df22d225 Fixing after merge 2023-12-15 22:44:24 +01:00
Kelvin cd4295be59 Merge branch 'video-cookie-support' into 'master'
Video Cookie Support & Static Request Modifiers

See merge request videostreaming/grayjay!11
2023-12-15 21:33:06 +00:00
Kelvin 7d366110b1 Merge branch 'wip-subgroups' into 'master'
Subscription Groups

See merge request videostreaming/grayjay!12
2023-12-15 21:28:03 +00:00
Kelvin 35c5045b3f Named presets 2023-12-15 20:10:28 +01:00
Kelvin 4930ea8183 Working subscription groups and image pickers 2023-12-15 19:38:33 +01:00
Kelvin 02292fed04 Progress 2023-12-13 23:28:34 +01:00
Koen bf6e61ed90 Fixed playback controls. 2023-12-13 17:14:40 +01:00
Koen 2ac8e0e621 Added casting controls to connected dialog. 2023-12-13 15:02:49 +01:00
Koen 0432f06eb3 Merge branch 'media3-migration' into 'master'
Media3 migration.

See merge request videostreaming/grayjay!10
2023-12-13 12:57:43 +00:00
Koen 7bfab8409f Made FCastCastingDevice more able to reconnect. 2023-12-13 13:47:38 +01:00
Koen 52d833d726 Added next and previous buttons to cast controls. 2023-12-13 13:40:11 +01:00
Koen 14d579eb1b Added support for playback rate changing when casting. 2023-12-13 13:28:51 +01:00
Koen d3ab8ecf3a Added support for automatically resuming casting device if stopped. 2023-12-13 12:48:39 +01:00
Koen 627b8c2b5d Implemented support for automatically connecting using QR code. 2023-12-13 12:36:16 +01:00
Koen 7f1cb22c12 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into media3-migration 2023-12-13 12:22:45 +01:00
Koen 5551bd31fe Added setting to allow full-screen portrait. 2023-12-13 12:18:54 +01:00
Koen 189d855c3f Automatic playback continuation should only happen when it was not paused or stopped. 2023-12-13 11:27:21 +01:00
Koen 0ab52e8f4d Fixed notification implementation. 2023-12-13 10:34:14 +01:00
Kelvin 27eb5aa6e1 WIP views 2023-12-12 22:20:38 +01:00
Kelvin 49b5b16641 Abstract RequestModifier, AdHocRequestModifier, RequestModifierOptions 2023-12-12 14:44:07 +01:00
Kelvin 73dd52af28 Working video cookies 2023-12-11 23:35:59 +01:00
Koen 3b8d256bad Media3 migration. 2023-12-11 17:54:18 +01:00
Kelvin 5d7dc1fdcb Language fixes, now uses standard language tags 2023-12-11 16:52:31 +01:00
Kelvin f31b6c50e9 Solving some warning 2023-12-11 13:26:13 +01:00
Koen fa12f8277c Reduced amount of warnings. 2023-12-11 11:51:43 +01:00
Koen 150a7d5006 Added proper permissions for download and export service (A14). 2023-12-10 09:29:33 +01:00
Koen a0a73a8e5c Fixes Android SDK. 2023-12-10 00:34:34 +01:00
Kelvin 4723a0b29a Fix up next view 2023-12-09 19:03:47 +01:00
Kelvin adbe0357ba Refs 2023-12-09 18:12:36 +01:00
Kelvin b0a35bcf3f No notification on known item, Polycentric logging, refs 2023-12-09 18:11:45 +01:00
Kelvin 0e7482321c Import platform redirect and disabled buttons, minor ui fixes 2023-12-09 16:31:34 +01:00
Koen e50d195b85 Fixed artwork not displaying properly. Loop button now hidden if you have a queue. Videos on queue editor now properly updates the amount of videos when a video is deleted. 2023-12-08 21:42:27 +01:00
Koen 33780f1046 Cleanup. 2023-12-08 14:53:57 +01:00
Koen 8b20b4909f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 14:43:33 +01:00
Koen 71a3828fe4 Migration to new deps. 2023-12-08 14:43:24 +01:00
Kelvin d713f2bd55 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 13:24:06 +01:00
Kelvin 069a615193 Polycentric persistent cache fixes for subscriptions 2023-12-08 13:23:58 +01:00
Koen f7d2cb4055 Updated Odysee. 2023-12-08 12:03:24 +01:00
Koen f109d82537 Fixed clickable area of likes/dislikes. 2023-12-08 12:02:17 +01:00
Kelvin ab49d4749b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:45:27 +01:00
Kelvin 507eed4f53 fix error message 2023-12-08 11:45:24 +01:00
Kelvin 23ca4addf9 Prevent dup queue items, handle toast more centrally 2023-12-08 11:40:06 +01:00
Koen 331ed09775 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:27:51 +01:00
Koen 85303b54bc Fixed bug in audio focus loss timers using the wrong time. 2023-12-08 11:27:42 +01:00
Kelvin f224cd1ca5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:26:07 +01:00
Kelvin d433d6e774 Elaboration on prev/next queue behavior 2023-12-08 11:25:35 +01:00
Koen 90de54ac5c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:24:25 +01:00
Koen 5ff8f1ba6d Connectivity loss fixes. 2023-12-08 11:24:17 +01:00
Kelvin bc00b12b8c Hide prev/next for single item queue, Fullscreen next/prev button show/hide, Bypass loop for next controls 2023-12-08 11:17:36 +01:00
Kelvin 1c0cfa89a3 Fixing Queue and hiding next/prev buttons 2023-12-08 10:44:20 +01:00
Kelvin efa1361fbe Remove (0/0) import, captcha delete update buttons 2023-12-07 22:04:24 +01:00
Kelvin 73918a8d76 refs 2023-12-07 20:18:39 +01:00
Kelvin a3c8bbb21f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-07 20:17:49 +01:00
Kelvin 53525cb365 Improved import flow, Empty pager view support, No subscriptions result view, LoginRequiredException support 2023-12-07 20:17:35 +01:00
Koen e4d39cbec4 Added stop all gestures flow. 2023-12-07 17:16:25 +01:00
Koen a15e4beafb Updated youtube ref for stable. 2023-12-07 17:02:19 +01:00
Koen d47298102e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-07 17:00:57 +01:00
Koen 280feea06e Default language fixes. 2023-12-07 17:00:47 +01:00
Kelvin f649d62e38 Logging and refs 2023-12-07 16:04:22 +01:00
Kelvin 0ae05e7cd4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-06 19:46:14 +01:00
Kelvin b284176072 Loop support, Improve add to queue behavior, home retry, fix history search pager, defaults progressbar 2023-12-06 19:46:09 +01:00
Koen 5fffaf2f4e Added next/previous skip buttons. 2023-12-06 19:40:05 +01:00
Koen 58da91eae8 Made history properly reload. 2023-12-06 16:47:22 +01:00
Koen 98d92d3fe2 Updated HistoryView to use pager. 2023-12-06 16:32:17 +01:00
Koen c5d35b27f0 Cleanup on store PR. 2023-12-06 13:41:07 +01:00
Koen aee5b75c2f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-06 10:34:32 +01:00
Koen fe02197bd8 Fixes to Polycentric flows. 2023-12-06 10:34:20 +01:00
Kelvin a1060a15be Merge 2023-12-05 21:04:59 +01:00
Kelvin dc7b2f420b Refs 2023-12-05 21:03:58 +01:00
Kelvin b35390a4bb Merge branch 'db-store' into 'master'
WIP DBStore

See merge request videostreaming/grayjay!8
2023-12-05 19:44:19 +00:00
Kelvin 3b253ad2b6 Merge 2023-12-05 20:41:06 +01:00
Kelvin 06c39ce973 QueryIn support, channel cache query grouped 2023-12-05 17:04:09 +01:00
Koen 11b8914615 Fixed polycentric disable fallback. 2023-12-05 16:10:51 +01:00
Koen e45c8617df Cleanup. 2023-12-05 15:21:35 +01:00
Koen 9075a2599c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-05 15:20:05 +01:00
Koen dd8d50e0e2 Added disable polycentric setting. 2023-12-05 15:19:54 +01:00
Kelvin 55a11d82ac Creator search, toggle to disable bad rep comments fading 2023-12-05 14:15:39 +01:00
Kelvin 7ee4f411cb Import dialog 2023-12-04 22:18:07 +01:00
Kelvin c9d5508018 Refs 2023-12-04 20:08:29 +01:00
Kelvin bef8fc682c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-04 20:07:32 +01:00
Kelvin c37d464403 Refs 2023-12-04 20:07:14 +01:00
Kelvin cbf2712654 ManagedDBSTore delete corrupted items, Fix serialized content serializer, Fix notifications wrong intent 2023-12-04 20:06:24 +01:00
Koen 08134b4427 If cache entry is expired, load it from cache, then reload it from live. 2023-12-04 17:09:27 +01:00
Koen f90290c4ec Added support for connecting to FCast via QR code. 2023-12-04 10:57:11 +01:00
Kelvin 7cde8ed538 Filler dev options 2023-12-01 16:45:17 +01:00
Kelvin 585cf090d6 DBStore improvements, query like support, more unittest, refactor into StateHistory, history indexes 2023-12-01 16:09:41 +01:00
Koen 23d1085755 Fixes to connectivity loss playback restart and fixes to added ensureEnoughContentVisible. 2023-12-01 15:01:41 +01:00
Koen fc5888d57e Added setting to allow restarting playback after connectivity loss behavior to be changed. 2023-12-01 14:11:15 +01:00
Kelvin c5541b1747 Working DBCache, test plugin 2023-11-30 20:58:37 +01:00
Koen 0fd8ba28bb Chromecast protobuf cleanup and fixed Odysee content-types being misrepresented causing casting to desktop to break. 2023-11-30 14:21:42 +01:00
Koen 6d9f4959e0 Removed Logger. 2023-11-30 11:50:52 +01:00
Koen 4be4bb631f Fixed gesture control issues causing wrong area to have gesture controls and disabled full screen gesture when casting. 2023-11-30 11:40:58 +01:00
Koen 948f5a2a6d Added FCast guide and other casting help options. 2023-11-30 09:56:40 +01:00
Koen baad342aec Fixed Rumble comments and show error in CommentList whenever an error happens. 2023-11-30 08:47:07 +01:00
Kelvin aeb29c54cd WIP Channel content cache 2023-11-30 00:12:46 +01:00
Koen a5dfa653ad Removed last mentions of FastCast and added backwards compatibility. 2023-11-29 16:02:58 +01:00
Koen 3387c727d1 Proper implementation for replies/likes/dislikes in the comment tab. 2023-11-29 15:48:34 +01:00
Koen c806ff2e33 Added support for comment deletion. 2023-11-29 13:54:26 +01:00
Koen 1db4d427fc Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-29 13:43:49 +01:00
Koen 3bf73ed5e8 Implemented delete comment support. Implemented Comments tab. Implemented replies overlay showing parent comment. 2023-11-29 13:43:40 +01:00
Gabe Rogan db44aa2c4d Merge branch 'quickstart-docs' into 'master'
Add quickstart docs

See merge request videostreaming/grayjay!7
2023-11-28 14:32:40 +00:00
Gabe Rogan 0e6e381800 Custom HTTP server wording 2023-11-28 09:31:46 -05:00
Gabe Rogan 69e43dc533 Split docs into multiple pages 2023-11-28 09:11:42 -05:00
Gabe Rogan ee4442d553 Add quickstart docs 2023-11-27 16:20:53 -05:00
Kelvin c49b9f7841 DBStore query support and tests 2023-11-27 17:38:55 +01:00
Koen 8a35cd0e82 Added settings to allow different behavior when audio focus is regained within 10 seconds. 2023-11-27 17:08:40 +01:00
Koen 0ae90ecf03 Updated Playstore flow for URL handling. 2023-11-27 16:27:56 +01:00
Koen 3d2840fe15 Merge branch 'hls-download' into 'master'
HLS download implementation

See merge request videostreaming/grayjay!6
2023-11-27 13:49:34 +00:00
Koen b6ad3fd991 HLS download implementation 2023-11-27 13:49:34 +00:00
Koen 2ee3c30b0e Better URL handling support. Prompt user to set Grayjay as a default handler for certain URLs. 2023-11-27 12:10:53 +01:00
Kelvin 662e94bcee Unittests and fixes for dbstore 2023-11-24 22:42:30 +01:00
Kelvin f3c9e0196e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into db-store 2023-11-24 15:22:34 +01:00
Kelvin f15eb9bf9e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-24 15:22:08 +01:00
Kelvin 12b2552185 Settings search, Fix nested video events, Adding setting descriptions for metered 2023-11-24 15:22:03 +01:00
Koen d245e20b14 Chromecast socket crash fix. 2023-11-24 11:24:52 +01:00
Koen e47349d010 Added OPTIONS headers where necessary and further HLS spec implementations. 2023-11-24 10:37:18 +01:00
Kelvin eb3dd854d4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-23 17:28:23 +01:00
Kelvin c529446219 Attempt to fetch live videos for offline videos 2023-11-23 17:28:14 +01:00
Koen fa2f8c3447 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-23 16:45:09 +01:00
Koen 840d1ae534 Fixes to adhere closer to the HLS spec and Twitch VODs no longer start at end. 2023-11-23 16:44:58 +01:00
Kelvin 2530c6eb58 Live chat improvements and fixes 2023-11-23 16:35:13 +01:00
Kelvin 869789f0e2 WIP 2023-11-23 16:03:25 +01:00
Koen ee3761c780 Added full support for HLS casting to Airplay. 2023-11-23 13:18:09 +01:00
Koen e4c89e9aa9 Extended HLS spec, fixes to YES NO booleans, started on implementing HLS stream combiner. 2023-11-23 12:48:16 +01:00
Koen 9d5888ddf7 Fixed VODs not working properly for YouTube and Twitch. 2023-11-23 11:48:50 +01:00
Koen ecc94920d7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-22 22:33:05 +01:00
Koen 5cafbf243e Fixed channel contents long press and fixed a crash due to time bars. 2023-11-22 22:32:44 +01:00
Kelvin f3fa208680 Kick subs fix, dedup fix 2023-11-22 18:04:29 +01:00
Kelvin 502602e27a Reordering progress bar settings 2023-11-22 16:50:54 +01:00
Kelvin 5054b093a4 Stable refs 2023-11-22 16:15:05 +01:00
Kelvin 0ffaec6bc2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-22 16:05:39 +01:00
Kelvin ef8ea9eecf Fix whitelist checking for dev-portal 2023-11-22 16:05:27 +01:00
Koen b09d22e479 Added historical time bars to videos. 2023-11-22 14:49:34 +01:00
Koen 01787b6229 Added backfill exception printing to announcements. 2023-11-22 12:46:39 +01:00
Koen 4c022698d3 Quality selection overlay now properly closes when pressing the back button. 2023-11-22 11:32:51 +01:00
Koen bfdcab0e84 Properly handle V1 encrypted secrets in the upgrade process from V0 to V1. 2023-11-22 11:21:18 +01:00
Koen aaea5cc963 Only close the app on closeSegment if there is no video playing. 2023-11-22 10:38:04 +01:00
Koen 23d9c33406 Added support for v6 Odysee URLs. 2023-11-22 10:27:35 +01:00
Koen fad1b216df Further extended HLS spec that is implemented. 2023-11-22 09:32:52 +01:00
Kelvin e221b508d3 Improved notifications, experimental scheduled notifications 2023-11-21 23:31:26 +01:00
Koen dfafac7d99 Merge branch 'hls-live-stream-proxy' into 'master'
Finished implementation of HLS proxying.

See merge request videostreaming/grayjay!5
2023-11-21 15:12:09 +00:00
Koen 2246f8cee2 Finished implementation of HLS proxying. 2023-11-21 15:12:09 +00:00
Kelvin b65fc594dc Working history DB implementation 2023-11-20 21:27:27 +01:00
Kelvin f52b731615 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into db-store 2023-11-20 14:24:48 +01:00
Koen 8661ff88c0 Another iteration on the HttpContext fix. 2023-11-20 12:14:03 +01:00
Kelvin 99c06c516f WIP Store/testing 2023-11-17 22:17:49 +01:00
Koen 0bba7fa373 Keep screen on fixes. 2023-11-17 16:44:16 +01:00
Kelvin 0c1822b118 Locked content support 2023-11-17 00:34:21 +01:00
Kelvin 10e3d2122f wip 2023-11-16 20:32:15 +01:00
Kelvin 6df8f84421 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-16 17:13:50 +01:00
Kelvin 7fa80ec048 Fix communication issues devportal 2023-11-16 17:13:23 +01:00
Koen b3f9b81984 Do not allow empty polycentric comments. 2023-11-16 15:51:34 +01:00
Kelvin 1393c489c1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-15 20:02:36 +01:00
Kelvin 640c2cbed0 Chapter accuracy now sub-second 2023-11-15 20:02:28 +01:00
Koen e55509f549 Merge branch 'casting-fixes' into 'master'
Content length is now set correctly for HttpConstantHandler.

See merge request videostreaming/grayjay!4
2023-11-15 16:18:38 +00:00
Koen 27c7fb0c12 Content length is now set correctly for HttpConstantHandler. 2023-11-15 16:18:38 +00:00
Koen 88f3815585 When clicking on a video it is added to queue instead of replacing queue. 2023-11-15 11:49:22 +01:00
Koen 2e9405cfdb Exit full screen swipe is now a down gesture. 2023-11-15 11:40:09 +01:00
Koen 9c1b543ed6 Added 'Add to new playlist' button in options menu. 2023-11-14 14:58:24 +01:00
Koen d34cb0f9c1 Added support for long-press gesture to open options menu. 2023-11-14 14:42:41 +01:00
Koen 116dc90d21 Changed the way the changelog is written. 2023-11-14 14:18:17 +01:00
Kelvin 17b9853bb6 Better subscription behavior reporting 2023-11-14 00:27:13 +01:00
Kelvin 8bfb8abd20 Fix notifications 2023-11-13 23:35:25 +01:00
Kelvin 9ee3f1f26e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-13 22:10:43 +01:00
Kelvin 5dcff29d8d Language on all activities, Fix plugin initial state, Allow rotation prevention bypass, Settings warning support 2023-11-13 22:10:31 +01:00
Koen 6cfbd0c8bf Added + Tax indicator 2023-11-13 15:58:01 +01:00
Koen 01d96cce16 Made export request folder to export to. 2023-11-13 15:49:55 +01:00
Koen 58c376f011 Fixed Rumble subscription import. 2023-11-13 12:06:46 +01:00
Kelvin 439d339330 Fix channel membership reset 2023-11-11 16:47:14 +01:00
Kelvin 44eacc2a47 Stable refs 2023-11-10 20:38:46 +01:00
Kelvin 8135d61398 Fix language issue 2023-11-10 20:34:43 +01:00
Kelvin 66208f8265 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-10 19:49:20 +01:00
Kelvin f52251e23a Hide creators, Fix hide video, 2023-11-10 19:49:09 +01:00
Koen dbea93efe5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-10 12:10:30 +01:00
Koen 3bf0740bd1 Fixed control cast on non-fullscreen. 2023-11-10 12:10:12 +01:00
Kelvin fa7f1b11f3 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-09 19:49:51 +01:00
Kelvin ff914bbdf4 Temporary subscription workarounds 2023-11-09 19:49:45 +01:00
Koen b822078d4b Fixed support tab falsely showing when a creator does not have a polycentric profile. 2023-11-09 19:12:54 +01:00
Kelvin 290d2ceb50 Polycentrif ref 2023-11-09 16:57:32 +01:00
Kelvin 8ec9025990 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-09 16:55:32 +01:00
Kelvin c4cf856dcd Stable submods 2023-11-09 16:55:26 +01:00
Koen 38bb4e25d3 Merge branch 'encryption-changes' into 'master'
Encryption changes, load more on import subscriptions.

See merge request videostreaming/grayjay!3
2023-11-09 15:04:55 +00:00
Koen 0de996d91c Encryption changes, load more on import subscriptions. 2023-11-09 15:04:54 +00:00
Kelvin 1f38c9b27d Logs 2023-11-09 15:46:22 +01:00
Kelvin 234f31b02d Logs and submods 2023-11-09 15:42:33 +01:00
Koen 00e40e8cd6 Merge branch 'encryption-provider-split' into 'master'
Encryption provider split.

See merge request videostreaming/grayjay!2
2023-11-08 15:06:29 +00:00
Koen 0bc6a43dc1 Encryption provider split. 2023-11-08 15:06:29 +00:00
Kelvin e7e0157fbc Stable refs 2023-11-08 16:06:07 +01:00
Kelvin 4cae1a41a5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-08 16:03:01 +01:00
Kelvin 4fa61e7f52 Additional plugin settings capabilities 2023-11-08 16:02:52 +01:00
Koen f02ac796f5 Updated YT stable. 2023-11-08 13:23:49 +01:00
Kelvin 22146a6bdc Playlists ui tweaks 2023-11-08 12:15:53 +01:00
Kelvin 5285eae01d Channel membership support and live chat donation support fix 2023-11-07 20:34:23 +01:00
Koen c47ca369e4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-07 17:16:19 +01:00
Koen f0b1f62bb1 Casting button no longer visible when disabled (may require restart). 2023-11-07 17:16:06 +01:00
Kelvin f7aa6d006e Add header to login activity with current url 2023-11-07 16:46:55 +01:00
Kelvin 6b67cd549f Fix chapters not getting cleared 2023-11-07 16:24:22 +01:00
Kelvin fc6bf85822 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-07 16:04:26 +01:00
Kelvin fbd9345cf8 Fix fallback to cache results 2023-11-07 16:04:19 +01:00
Koen 63137b4c4d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-07 16:03:05 +01:00
Koen e28dc7a3a6 Crash fix for bottom menu bar for specific screen dimensions. 2023-11-07 16:02:55 +01:00
Kelvin 6e14acc685 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-07 15:43:21 +01:00
Kelvin ba64153f1d Language setting, Preview setting, Background audio switch setting, No error on comments failed 2023-11-07 15:43:13 +01:00
Koen 72c04e7556 Added casting button in full screen. 2023-11-07 15:08:33 +01:00
Koen 54f37ee5b2 Fixed crash. 2023-11-07 15:01:22 +01:00
Koen 4fbb325313 Added fix for video player not filling height properly for audio only in a playlist. 2023-11-07 14:20:57 +01:00
Koen e1d3b95f73 Fixed crash on Android 9 when playing a video. 2023-11-07 13:35:13 +01:00
Koen 8f7b4b8257 Merge branch 'monetization' into 'master'
Monetization

See merge request videostreaming/grayjay!1
2023-11-07 12:10:40 +00:00
Koen 9d906025ea Monetization 2023-11-07 12:10:40 +00:00
Kelvin d7f4dd65e8 Stable refs 2023-11-06 14:58:11 +01:00
Kelvin 599b119e62 Remove plugin interaction on main thread for channels 2023-11-06 14:53:24 +01:00
Kelvin 41176464db Fix missing swipe to refresh on tab switch 2023-11-06 14:43:24 +01:00
Kelvin dd0ad19fb9 NewLine subs import, fix no-recent video subscriptions 2023-11-06 14:25:09 +01:00
Kelvin 430625d2fb Fix icon colors 2023-11-06 13:37:18 +01:00
Kelvin 796cd1a776 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-06 13:20:42 +01:00
Kelvin baa26af0c0 Only show sub toasts when on subs page, WIP import ui 2023-11-06 13:20:33 +01:00
Koen ea0c27936e Fixed videos not automatically going to next video in playlist when casting. 2023-11-05 15:13:57 +01:00
Kelvin 4aade35d19 Grayjay schema channel support 2023-11-04 18:42:04 +01:00
Kelvin 251a5701af Custom grayjay open video url handling 2023-11-04 18:31:01 +01:00
Kelvin 2da3116111 Fix initial selection of subscription settings 2023-11-03 20:07:08 +01:00
Kelvin 4c82fa1a4a Stable refs 2023-11-03 18:25:40 +01:00
Kelvin 7eef6eece2 Primary claim support, fix sub for clients without type 2023-11-03 18:17:04 +01:00
Kelvin 570f32e980 PlatformUrl support 2023-11-03 15:39:27 +01:00
Kelvin 16a0351125 Per-plugin ratelimit setting 2023-11-03 15:15:18 +01:00
Kelvin 2fa9005806 Keep plugin settings on update 2023-11-03 14:46:43 +01:00
Kelvin 25527997fa Fix channels updating while they shouldnt 2023-11-03 14:37:36 +01:00
Kelvin 4655d8369d Reduce subscription calls, Improve subs sorting, Improve view sorting 2023-11-03 13:34:23 +01:00
Kelvin aeaaace3a4 Subscription settings from creators tab 2023-11-02 23:42:51 +01:00
Kelvin e6997004ff Fix new user crash, show/hide subscription settings button on change, raise import limit to 90 2023-11-02 23:22:42 +01:00
Kelvin 5e1896b7f2 Stable ref 2023-11-02 22:52:29 +01:00
Kelvin 88ca90c13a Notification improvements, Polycentric subscription parallelization, Cache load parallelization 2023-11-02 22:23:24 +01:00
Kelvin f8ee340499 Creator sort options views and watchtime, subscription header ordered by views, view/watchtime tracking for subscriptions, optional view/watchtime metrics in creator tab, cache channel results if subscribed, update subs only if older than 5 min 2023-11-02 20:21:26 +01:00
Kelvin 93f5260e20 Working smart subscriptions, Direct url through search, channel content cache trimming, skippable and skip chapter support, reinstall button for embedded plugins 2023-11-01 20:32:51 +01:00
Kelvin 34ba44ffa4 WIP Subscription notifs 2023-11-01 00:36:01 +01:00
Kelvin b3a3e459a4 WIP Smart subscriptions 2023-11-01 00:09:05 +01:00
Kelvin f234564952 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-10-30 19:22:22 +01:00
Kelvin ffa5795cc9 WIP new subscription system and ui 2023-10-30 19:22:17 +01:00
Koen 4f50c51356 Fixed context crash. 2023-10-30 14:37:49 +01:00
Koen 9e9c8a0bec Fixed Polycentric not backfilling issue 2023-10-30 11:44:56 +01:00
Koen 1349358d7c Added QRCaptureActivity and AudioNoisyReceiver to manifest. 2023-10-30 11:21:51 +01:00
Koen 9c50f15be7 Processed community feedback on German translations. Thank you McIrco95. 2023-10-30 11:08:29 +01:00
Koen 31e771daca Processed community feedback on german translations. Thank you Allstreamer. 2023-10-30 10:59:24 +01:00
Koen 66ce156dea Processed community feedback on translations. Thank you jorpilo. 2023-10-30 10:47:40 +01:00
Koen db6756bc78 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-10-30 09:22:34 +01:00
Koen cab2581476 Added more logging related to backfill and made backfill properly throw. 2023-10-30 09:22:25 +01:00
Kelvin 4c0be35020 Fix ContentTypes docs 2023-10-29 18:28:52 +00:00
Koen 7114201c08 Translations 2023-10-27 20:01:22 +02:00
Kelvin d8aecd325b Basic chapter system working 2023-10-25 20:38:57 +02:00
Koen 1d18c13817 Updated submodule. 2023-10-25 20:05:03 +02:00
Koen f65eb0cd53 Finished moving view strings to strings.xml 2023-10-25 19:54:48 +02:00
Koen 206c3884e9 Finished moving strings to strings.xml for activities and fragments. 2023-10-25 14:53:50 +02:00
Koen 35f9173980 Started moving all strings to strings.xml 2023-10-25 12:16:58 +02:00
Kelvin 48ab77eadc Patreon refs 2023-10-24 23:58:39 +02:00
Kelvin f486513105 Casting HLS fixed 2023-10-24 23:10:15 +02:00
Kelvin f338adf033 Fix polycentric profile content ordering and deduplication 2023-10-24 22:16:10 +02:00
Kelvin 74be667114 Retain login and captcha on embedded update, Play entire feed option 2023-10-24 14:47:34 +02:00
Kelvin b5a1fc92dc Add misisng synchronization, unsub all dev action, Dedup capital insensitive and more scaling max video date difference 2023-10-23 22:38:13 +02:00
Kelvin 9cec1a8c49 Stable ref updates 2023-10-23 21:03:23 +02:00
Kelvin d4afba929b Fix captcha, FAQ, issues page, icons on settings buttons 2023-10-23 20:36:26 +02:00
Koen 70939cbac6 Fixed log submission and added telemetry OS version. 2023-10-23 16:31:50 +02:00
Koen a3aa61df6d Fixed Odysee get channel contents. 2023-10-23 15:24:55 +02:00
Koen e13ab5cb40 Deduplicated map. 2023-10-23 15:23:46 +02:00
Koen d059947925 Odysee now works with more different types of channel URLs. 2023-10-23 14:24:28 +02:00
Koen d6c4b730de Fixes to Polycentric data display. 2023-10-23 14:21:10 +02:00
Koen 8241863170 Fixed comment alpha. 2023-10-21 16:06:05 +02:00
707 changed files with 42649 additions and 9158 deletions
+80
View File
@@ -0,0 +1,80 @@
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
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Need a Grayjay License?
url: https://pay.futo.org/api/PaymentPortal
about: Purchase a Grayjay license with FutoPay
- name: Plugin Building, Usage, or other Questions
url: https://chat.futo.org/#narrow/stream/46-Grayjay
about: Grayjays Community Chat
@@ -0,0 +1,63 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["documentation", "new"]
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)
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)
- type: textarea
id: grayjay-affected-pages
attributes:
label: Affected Pages
description: |
Link to or describe the pages relevant to your documentation change request.
placeholder:
value:
validations:
required: false
- type: textarea
id: grayjay-problem
attributes:
label: What is the docs issue?
description: What problems or suggestions do you have about the documentation?
placeholder:
value:
validations:
required: true
- type: textarea
id: grayjay-proposal
attributes:
label: Proposal
description: What documentation changes would fix this issue and where would you expect to find them? Are one or more page headings unclear? Do one or more pages need additional context, examples, or warnings? Do we need a new page or section dedicated to a specific topic? Your ideas help us understand what you and other users need from our documentation and how we can improve the content.
placeholder:
value:
validations:
required: false
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other open or closed GitLab/GitHub issues related to the problem or solution you described? If so, list them below. For example:
```
- #6017
```
placeholder:
value:
validations:
required: false
- 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.
@@ -0,0 +1,58 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
labels: ["enhancement", "new"]
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)
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
- type: textarea
id: grayjay-use-case
attributes:
label: Use Cases
description: |
In order to properly evaluate a feature request, it is necessary to understand the use cases for it. Please describe below the _end goal_ you are trying to achieve that has led you to request this feature. Please keep this section focused on the problem and not on the suggested solution.
placeholder:
value:
validations:
required: true
- type: textarea
id: grayjay-proposal
attributes:
label: Proposal
description: |
If you have an idea for a way to address the problem via a change to Grayjay features, please describe it below.
In this section, it's helpful to include specific examples of how what you are suggesting might look in the application, this allows us to understand the full picture of what you are proposing. If you're not sure of some details, don't worry! When we evaluate the feature request we may suggest modifications as necessary to work within the design constraints of the Grayjay Core Application.
placeholder:
value:
validations:
required: false
- 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 or to the suggested solution? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- 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.
+34
View File
@@ -0,0 +1,34 @@
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 }}
+24 -3
View File
@@ -1,9 +1,6 @@
[submodule "dep/polycentricandroid"]
path = dep/polycentricandroid
url = ../polycentricandroid.git
[submodule "app/src/playstore/assets/sources/peertube"]
path = app/src/playstore/assets/sources/peertube
url = ../plugins/peertube.git
[submodule "app/src/stable/assets/sources/kick"]
path = app/src/stable/assets/sources/kick
url = ../plugins/kick.git
@@ -61,3 +58,27 @@
[submodule "dep/futopay"]
path = dep/futopay
url = ../futopayclientlibraries.git
[submodule "app/src/unstable/assets/sources/bilibili"]
path = app/src/unstable/assets/sources/bilibili
url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/bilibili"]
path = app/src/stable/assets/sources/bilibili
url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/spotify"]
path = app/src/stable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/unstable/assets/sources/spotify"]
path = app/src/unstable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/stable/assets/sources/bitchute"]
path = app/src/stable/assets/sources/bitchute
url = ../plugins/bitchute.git
[submodule "app/src/unstable/assets/sources/bitchute"]
path = app/src/unstable/assets/sources/bitchute
url = ../plugins/bitchute.git
[submodule "app/src/unstable/assets/sources/dailymotion"]
path = app/src/unstable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
[submodule "app/src/stable/assets/sources/dailymotion"]
path = app/src/stable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
+1 -1
View File
@@ -19,7 +19,7 @@ Thank you for your interest in contributing! This document outlines how you can
### License
The official plugins for this project are licensed under GPLv3. Any contributions you make will also fall under the GPLv3 license.
The official plugins for this project are licensed under AGPL. Any contributions you make will also fall under the AGPL license.
### How to Contribute
-32
View File
@@ -1,32 +0,0 @@
# FUTO TEMPORARY LICENSE
This license grants you the rights, and only the rights, set out below in respect of the source code provided. If you take advantage of these rights, you accept this license. If you do not accept the license, do not access the code.
Words used in the Terms of Service have the same meaning in this license. Where there is any inconsistency between this license and those Terms of Service, these terms prevail.
## Section 1: Definitions
- "code" means the source code made available from time, in our sole discretion, for access under this license. Reference to code in this license means the code and any part of it and any derivative of it.
- “compilation” means to compile the code from source code to machine code.
- "defect" means a defect, bug, backdoor, security issue or other deficiency in the code.
- “non-commercial distribution” means distribution of the code or any compilation of the code, or of any other application or program containing the code or any compilation of the code, where such distribution is not intended for or directed towards commercial advantage or monetary compensation.
- "review" means to access, analyse, test and otherwise review the code as a reference, for the sole purpose of analysing it for defects.
- "you" means the licensee of rights set out in this license.
## Section 2: Grant of Rights
1. Subject to the terms of this license, we grant you a non-transferable, non-exclusive, worldwide, royalty-free license to access and use the code solely for the purposes of review, compilation and non-commercial distribution.
2. You may provide the code to anyone else and publish excerpts of it for the purposes of review, compilation and non-commercial distribution, provided that when you do so you make any recipient of the code aware of the terms of this license, they must agree to be bound by the terms of this license and you must attribute the code to the provider.
3. Other than in respect of those parts of the code that were developed by other parties and as specified strictly in accordance with the open source and other licenses under which those parts of the code have been made available, as set out on our website or in those items of code, you are not entitled to use or do anything with the code for any commercial or other purpose, other than review, compilation and non-commercial distribution in accordance with the terms of this license.
4. Subject to the terms of this license, you must at all times comply with and shall be bound by our Terms of Use, Privacy and Data Policy.
## Section 3: Limitations
1. This license does not grant you any rights to use the provider's name, logo, or trademarks and you must not in any way indicate you are authorised to speak on behalf of the provider.
2. If you issue proceedings in any jurisdiction against the provider because you consider the provider has infringed copyright or any patent right in respect of the code (including any joinder or counterclaim), your license to the code is automatically terminated.
3. THE CODE IS MADE AVAILABLE "AS-IS" AND WITHOUT ANY EXPRESS OR IMPLIED GUARANTEES AS TO FITNESS, MERCHANTABILITY, NON-INFRINGEMENT OR OTHERWISE. IT IS NOT BEING PROVIDED IN TRADE BUT ON A VOLUNTARY BASIS ON OUR PART AND IS NOT MADE AVAILABLE FOR ANY USE OUTSIDE THE TERMS OF THIS LICENSE. ANYONE ACCESSING THE CODE MUST ENSURE THEY HAVE THE REQUISITE EXPERTISE TO SECURE THEIR OWN SYSTEM AND DEVICES AND TO ACCESS AND USE THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE. YOU BEAR THE RISK OF ACCESSING AND USING THE CODE. IN PARTICULAR, THE PROVIDER BEARS NO LIABILITY FOR ANY INTERFERENCE WITH OR ADVERSE EFFECT ON YOUR SYSTEM OR DEVICES AS A RESULT OF YOUR ACCESSING AND USING THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE OR OTHERWISE.
## Section 4: Termination, suspension and variation
1. We may suspend, terminate or vary the terms of this license and any access to the code at any time, without notice, for any reason or no reason, in respect of any licensee, group of licensees or all licensees including as may be applicable any sub-licensees.
## Section 5: General
1. This license and its interpretation and operation are governed solely by the local law. You agree to submit to the exclusive jurisdiction of the local arbitral tribunals as further described in our Terms of Service and you agree not to raise any jurisdictional issue if we need to enforce an arbitral award or judgment in our jurisdiction or another country.
2. Questions and comments regarding this license are welcomed and should be addressed at https://chat.futo.org/login/.
Last updated 7 June 2023.
+43
View File
@@ -0,0 +1,43 @@
# Grayjay Core License 1.0
## Acceptance
By using the software, you agree to all of the terms and conditions below.
## Copyright License
FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
## Limitations
You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.
Notwithstanding the above, you may not remove or obscure any functionality in the software related to payment to the Licensor in any copy you distribute to others.
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.
## 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.
## Fair Use
You may have "fair use" rights for the software under the law. These terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the Licensor from granting licenses to anyone else. These terms do not imply any other licenses.
## Termination
If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently.
## No Liability
As far as the law allows, the software comes as is, without any warranty or condition, and the Licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
## Definitions
- The “Licensor” is the entity offering these terms, FUTO Holdings, Inc.
- The “software” is the software the licensor makes available under these terms, including any portion of it.
- “You” refers to the individual or entity agreeing to these terms.
- “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
- “Your license” is the license granted to you for the software under these terms.
- “Use” means anything you do with the software requiring your license.
- “Trademark” means trademarks, service marks, and similar rights.
+59 -35
View File
@@ -1,10 +1,11 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
id 'org.ajoberstar.grgit' version '1.7.2'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
id 'org.ajoberstar.grgit' version '5.2.2'
id 'com.google.protobuf'
id 'kotlin-parcelize'
id 'com.google.devtools.ksp'
}
ext {
@@ -23,7 +24,7 @@ if (keystorePropertiesFile.exists()) {
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.22.3'
artifact = 'com.google.protobuf:protoc:3.25.1'
}
generateProtoTasks {
all().each { task ->
@@ -38,7 +39,7 @@ protobuf {
android {
namespace 'com.futo.platformplayer'
compileSdk 33
compileSdk 34
flavorDimensions "buildType"
productFlavors {
stable {
@@ -96,11 +97,15 @@ android {
defaultConfig {
minSdk 28
targetSdk 33
targetSdk 34
versionCode gitVersionCode
versionName gitVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
signingConfigs {
@@ -136,72 +141,91 @@ android {
universalApk true
}
}
buildFeatures {
buildConfig true
}
sourceSets {
main {
assets {
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
}
}
}
}
dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
//Images
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
implementation 'com.github.bumptech.glide:glide:4.15.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
//Async
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
//HTTP
implementation "com.squareup.okhttp3:okhttp:4.10.0"
implementation "com.squareup.okhttp3:okhttp:4.11.0"
//JSON
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" //Used for structured json
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS
implementation("com.caoccao.javet:javet-android:2.2.1")
implementation("com.caoccao.javet:javet-android:3.0.2")
//Exoplayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.7'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7'
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.18.7'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.7'
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.18.7'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
implementation 'androidx.media3:media3-ui:1.2.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
implementation 'androidx.media3:media3-transformer:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.media:media:1.7.0'
//Other
implementation 'org.jmdns:jmdns:3.5.1'
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 'org.jetbrains.kotlin:kotlin-reflect:1.7.20'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1'
implementation 'com.journeyapps:zxing-android-embedded:4.2.0'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'com.caverock:androidsvg-aar:1.4'
//Protobuf
implementation 'com.google.protobuf:protobuf-javalite:3.22.3'
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
implementation 'com.polycentric.core:app:1.0'
implementation 'com.futo.futopay:app:1.0'
implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
//Database
implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
//Payment
implementation 'com.stripe:stripe-android:20.28.3'
implementation 'com.stripe:stripe-android:20.35.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.20"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
@@ -0,0 +1,94 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "ffba56c2f572c25080ce8596e8bb8945",
"entities": [
{
"tableName": "history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT NOT NULL, `position` INTEGER NOT NULL, `datetime` INTEGER NOT NULL, `name` TEXT NOT NULL, `serialized` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_history_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_url` ON `${TABLE_NAME}` (`url`)"
},
{
"name": "index_history_name",
"unique": false,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_history_datetime",
"unique": false,
"columnNames": [
"datetime"
],
"orders": [
"DESC"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffba56c2f572c25080ce8596e8bb8945')"
]
}
}
@@ -0,0 +1,88 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "eb813d54b9c44d29f1d7bb198a16d4d1",
"entities": [
{
"tableName": "subscription_cache",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT, `channelUrl` TEXT, `datetime` INTEGER, `serialized` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "channelUrl",
"columnName": "channelUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_subscription_cache_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_url` ON `${TABLE_NAME}` (`url`)"
},
{
"name": "index_subscription_cache_channelUrl",
"unique": false,
"columnNames": [
"channelUrl"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_channelUrl` ON `${TABLE_NAME}` (`channelUrl`)"
},
{
"name": "index_subscription_cache_datetime",
"unique": false,
"columnNames": [
"datetime"
],
"orders": [
"DESC"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb813d54b9c44d29f1d7bb198a16d4d1')"
]
}
}
@@ -0,0 +1,52 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "6e3b2d286325c4ea8a7a4c94c290daec",
"entities": [
{
"tableName": "testing",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`someString` TEXT NOT NULL, `someNum` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `serialized` BLOB)",
"fields": [
{
"fieldPath": "someString",
"columnName": "someString",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "someNum",
"columnName": "someNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3b2d286325c4ea8a7a4c94c290daec')"
]
}
}
@@ -0,0 +1,111 @@
package com.futo.platformplayer
import android.util.Base64
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.futo.platformplayer.casting.FCastCastingDevice
import com.futo.platformplayer.casting.Opcode
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
import com.futo.platformplayer.casting.models.FCastPlayMessage
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import javax.crypto.spec.SecretKeySpec
@RunWith(AndroidJUnit4::class)
class FCastEncryptionTests {
@Test
fun testDHEncryptionSelf() {
val keyPair1 = FCastCastingDevice.generateKeyPair()
val keyPair2 = FCastCastingDevice.generateKeyPair()
Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})")
val keyExchangeMessage1 = FCastCastingDevice.getKeyExchangeMessage(keyPair1)
val keyExchangeMessage2 = FCastCastingDevice.getKeyExchangeMessage(keyPair2)
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
val aesKey1 = FCastCastingDevice.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
val aesKey2 = FCastCastingDevice.computeSharedSecret(keyPair2.private, keyExchangeMessage1)
assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP))
Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}")
val message = FCastPlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testAESKeyGeneration() {
val cases = listOf(
listOf(
//Public other
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
//Private self
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
//AES
"7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
),
listOf(
//Public other
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
//Private self
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
//AES
"a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
)
)
for (case in cases) {
val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP)
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, case[0])
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
}
}
@Test
fun testDHEncryptionKnown() {
val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP)
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=")
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
val message = FCastPlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testDecryptMessageKnown() {
val encryptedMessage = Json.decodeFromString<FCastEncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
}
}
@@ -1,13 +1,14 @@
package com.futo.platformplayer
import com.futo.platformplayer.encryption.EncryptionProvider
import com.futo.platformplayer.encryption.GEncryptionProviderV0
import com.futo.platformplayer.encryption.GEncryptionProviderV1
import junit.framework.TestCase.assertEquals
import org.junit.Test
class EncryptionProviderTests {
class GEncryptionProviderTests {
@Test
fun testEncryptDecrypt() {
val encryptionProvider = EncryptionProvider.instance
fun testEncryptDecryptV1() {
val encryptionProvider = GEncryptionProviderV1.instance
val plaintext = "This is a test string."
// Encrypt the plaintext
@@ -22,8 +23,8 @@ class EncryptionProviderTests {
@Test
fun testEncryptDecryptBytes() {
val encryptionProvider = EncryptionProvider.instance
fun testEncryptDecryptBytesV1() {
val encryptionProvider = GEncryptionProviderV1.instance
val bytes = "This is a test string.".toByteArray();
// Encrypt the plaintext
@@ -36,21 +37,36 @@ class EncryptionProviderTests {
assertArrayEquals(bytes, decrypted);
}
@Test
fun testEncryptDecryptBytesPassword() {
val encryptionProvider = EncryptionProvider.instance
val bytes = "This is a test string.".toByteArray();
val password = "1234".padStart(32, '9');
fun testEncryptDecryptV0() {
val encryptionProvider = GEncryptionProviderV0.instance
val plaintext = "This is a test string."
// Encrypt the plaintext
val ciphertext = encryptionProvider.encrypt(bytes, password)
val ciphertext = encryptionProvider.encrypt(plaintext)
// Decrypt the ciphertext
val decrypted = encryptionProvider.decrypt(ciphertext, password)
val decrypted = encryptionProvider.decrypt(ciphertext)
// The decrypted string should be equal to the original plaintext
assertEquals(plaintext, decrypted)
}
@Test
fun testEncryptDecryptBytesV0() {
val encryptionProvider = GEncryptionProviderV0.instance
val bytes = "This is a test string.".toByteArray();
// Encrypt the plaintext
val ciphertext = encryptionProvider.encrypt(bytes)
// Decrypt the ciphertext
val decrypted = encryptionProvider.decrypt(ciphertext)
// The decrypted string should be equal to the original plaintext
assertArrayEquals(bytes, decrypted);
}
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
@@ -0,0 +1,45 @@
package com.futo.platformplayer
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV1
import junit.framework.TestCase.assertEquals
import org.junit.Test
class GPasswordEncryptionProviderTests {
@Test
fun testEncryptDecryptBytesPasswordV1() {
val encryptionProvider = GPasswordEncryptionProviderV1();
val bytes = "This is a test string.".toByteArray();
// Encrypt the plaintext
val ciphertext = encryptionProvider.encrypt(bytes, "1234")
// Decrypt the ciphertext
val decrypted = encryptionProvider.decrypt(ciphertext, "1234")
// The decrypted string should be equal to the original plaintext
assertArrayEquals(bytes, decrypted);
}
@Test
fun testEncryptDecryptBytesPasswordV0() {
val encryptionProvider = GPasswordEncryptionProviderV0("1234".padStart(32, '9'));
val bytes = "This is a test string.".toByteArray();
// Encrypt the plaintext
val ciphertext = encryptionProvider.encrypt(bytes)
// Decrypt the ciphertext
val decrypted = encryptionProvider.decrypt(ciphertext)
// The decrypted string should be equal to the original plaintext
assertArrayEquals(bytes, decrypted);
}
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
assertEquals(a.size, b.size);
for(i in 0 until a.size) {
assertEquals(a[i], b[i]);
}
}
}
@@ -0,0 +1,368 @@
package com.futo.platformplayer
import androidx.test.platform.app.InstrumentationRegistry
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.testing.DBTOs
import org.junit.Assert
import org.junit.Test
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
class ManagedDBStoreTests {
val context = InstrumentationRegistry.getInstrumentation().targetContext;
@Test
fun startup() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
store.shutdown();
}
@Test
fun insert() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
createAndAssert(store, testObj);
store.shutdown();
}
@Test
fun update() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
val obj = createAndAssert(store, testObj);
testObj.someStr = "Testing";
store.update(obj.id!!, testObj);
val obj2 = store.get(obj.id!!);
assertIndexEquals(obj2, testObj);
store.shutdown();
}
@Test
fun delete() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
val obj = createAndAssert(store, testObj);
store.delete(obj.id!!);
Assert.assertEquals(store.count(), 0);
Assert.assertNull(store.getOrNull(obj.id!!));
store.shutdown();
}
@Test
fun withIndex() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({it.someString}, index, true)
.load(context, true);
store.deleteAll();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
val obj3 = createAndAssert(store, testObj3);
Assert.assertEquals(store.count(), 3);
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertTrue(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
Assert.assertEquals(index.size, 3);
val oldStr = testObj1.someStr;
testObj1.someStr = UUID.randomUUID().toString();
store.update(obj1.id!!, testObj1);
Assert.assertEquals(index.size, 3);
Assert.assertFalse(index.containsKey(oldStr));
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertTrue(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
store.delete(obj2.id!!);
Assert.assertEquals(index.size, 2);
Assert.assertFalse(index.containsKey(oldStr));
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertFalse(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
store.shutdown();
}
@Test
fun withUnique() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({it.someString}, index, false, true)
.load(context, true);
store.deleteAll();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
testObj3.someStr = testObj2.someStr;
Assert.assertEquals(store.insert(testObj3), obj2.id!!);
Assert.assertEquals(store.count(), 2);
store.shutdown();
}
@Test
fun getPage() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObjs = createSequence(store, 25);
val page1 = store.getPage(0, 10);
val page2 = store.getPage(1, 10);
val page3 = store.getPage(2, 10);
Assert.assertEquals(10, page1.size);
Assert.assertEquals(10, page2.size);
Assert.assertEquals(5, page3.size);
store.shutdown();
}
@Test
fun query() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testStr = UUID.randomUUID().toString();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val testObj4 = DBTOs.TestObject();
testObj3.someStr = testStr;
testObj4.someStr = testStr;
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
val obj3 = createAndAssert(store, testObj3);
val obj4 = createAndAssert(store, testObj4);
val results = store.query(DBTOs.TestIndex::someString, testStr);
Assert.assertEquals(2, results.size);
for(result in results) {
if(result.someNum == obj3.someNum)
assertIndexEquals(obj3, result);
else
assertIndexEquals(obj4, result);
}
store.shutdown();
}
@Test
fun queryPage() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({ it.someNum }, index)
.load(context, true);
store.deleteAll();
val testStr = UUID.randomUUID().toString();
val testResults = createSequence(store, 40, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStr;
});
val page1 = store.queryPage(DBTOs.TestIndex::someString, testStr, 0,10);
val page2 = store.queryPage(DBTOs.TestIndex::someString, testStr, 1,10);
val page3 = store.queryPage(DBTOs.TestIndex::someString, testStr, 2,10);
Assert.assertEquals(10, page1.size);
Assert.assertEquals(10, page2.size);
Assert.assertEquals(0, page3.size);
store.shutdown();
}
@Test
fun queryPager() {
val testStr = UUID.randomUUID().toString();
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStr;
}) {
val pager = it.queryPager(DBTOs.TestIndex::someString, testStr, 10);
val items = pager.getResults().toMutableList();
while(pager.hasMorePages()) {
pager.nextPage();
items.addAll(pager.getResults());
}
Assert.assertEquals(50, items.size);
for(i in 0 until 50) {
val k = i * 2;
Assert.assertEquals(k, items[i].someNum);
}
}
}
@Test
fun queryLike() {
val testStr = UUID.randomUUID().toString();
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStrLike;
}) {
val results = it.queryLike(DBTOs.TestIndex::someString, "%Testing%");
Assert.assertEquals(50, results.size);
}
}
@Test
fun queryLikePager() {
val testStr = UUID.randomUUID().toString();
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStrLike;
}) {
val pager = it.queryLikePager(DBTOs.TestIndex::someString, "%Testing%", 10);
val items = pager.getResults().toMutableList();
while(pager.hasMorePages()) {
pager.nextPage();
items.addAll(pager.getResults());
}
Assert.assertEquals(50, items.size);
for(i in 0 until 50) {
val k = i * 2;
Assert.assertEquals(k, items[i].someNum);
}
}
}
@Test
fun queryGreater() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.queryGreater(DBTOs.TestIndex::someNum, 51);
Assert.assertEquals(48, results.size);
}
}
@Test
fun querySmaller() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.querySmaller(DBTOs.TestIndex::someNum, 30);
Assert.assertEquals(30, results.size);
}
}
@Test
fun queryBetween() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.queryBetween(DBTOs.TestIndex::someNum, 30, 65);
Assert.assertEquals(34, results.size);
}
}
@Test
fun queryIn() {
val ids = mutableListOf<String>()
testQuery(1100, { i, testObject ->
testObject.someNum = i;
ids.add(testObject.someStr);
}) {
val pager = it.queryInPager(DBTOs.TestIndex::someString, ids.take(1000), 65);
val list = mutableListOf<Any>();
list.addAll(pager.getResults());
while(pager.hasMorePages())
{
pager.nextPage();
list.addAll(pager.getResults());
}
Assert.assertEquals(1000, list.size);
}
}
private fun testQuery(items: Int, modifier: (Int, DBTOs.TestObject)->Unit, testing: (ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>)->Unit) {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
createSequence(store, items, modifier);
try {
testing(store);
}
finally {
store.shutdown();
}
}
private fun createSequence(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List<DBTOs.TestIndex> {
val list = mutableListOf<DBTOs.TestIndex>();
for(i in 0 until count) {
val obj = DBTOs.TestObject();
obj.someNum = i;
modifier?.invoke(i, obj);
list.add(createAndAssert(store, obj));
}
return list;
}
private fun createAndAssert(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, obj: DBTOs.TestObject): DBTOs.TestIndex {
val id = store.insert(obj);
Assert.assertTrue(id > 0);
val dbObj = store.get(id);
assertIndexEquals(dbObj, obj);
return dbObj;
}
private fun assertObjectEquals(obj1: DBTOs.TestObject, obj2: DBTOs.TestObject) {
Assert.assertEquals(obj1.someStr, obj2.someStr);
Assert.assertEquals(obj1.someNum, obj2.someNum);
}
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestObject) {
Assert.assertEquals(obj1.someString, obj2.someStr);
Assert.assertEquals(obj1.someNum, obj2.someNum);
assertObjectEquals(obj1.obj, obj2);
}
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestIndex) {
Assert.assertEquals(obj1.someString, obj2.someString);
Assert.assertEquals(obj1.someNum, obj2.someNum);
assertIndexEquals(obj1, obj2.obj);
}
class Descriptor: ManagedDBDescriptor<DBTOs.TestObject, DBTOs.TestIndex, DBTOs.DB, DBTOs.DBDAO>() {
override val table_name: String = "testing";
override fun indexClass(): KClass<DBTOs.TestIndex> = DBTOs.TestIndex::class;
override fun dbClass(): KClass<DBTOs.DB> = DBTOs.DB::class;
override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj);
}
}
+63 -24
View File
@@ -7,8 +7,14 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
<application
android:allowBackup="true"
@@ -20,7 +26,8 @@
android:supportsRtl="true"
android:theme="@style/Theme.FutoVideo"
android:usesCleartextTraffic="true"
tools:targetApi="31">
tools:targetApi="31"
android:largeHeap="true">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/authority"
@@ -33,17 +40,18 @@
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
<service android:name=".services.DownloadService"
android:enabled="true" />
<service android:name=".services.ExportingService"
android:enabled="true" />
android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" />
<receiver android:name=".receivers.PlannedNotificationReceiver" />
<activity
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask"
android:resizeableActivity="true"
@@ -57,6 +65,14 @@
<data android:scheme="grayjay" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="fcast" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -91,6 +107,26 @@
<data android:host="*" />
<data android:scheme="file" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:autoVerify="true">
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<data android:host="*" />
<data android:scheme="content" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:autoVerify="true">
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<data android:host="*" />
<data android:scheme="file" />
<data android:mimeType="application/zip" />
</intent-filter>
<intent-filter android:autoVerify="true">
@@ -117,27 +153,27 @@
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.DeveloperActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.ExceptionActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.CaptchaActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.LoginActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.AddSourceActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter>
@@ -151,41 +187,44 @@
</activity>
<activity
android:name=".activities.AddSourceOptionsActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricHomeActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricBackupActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricCreateProfileActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricProfileActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricWhyActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricImportProfileActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.ManageTabsActivity"
android:screenOrientation="portrait"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.AddSourceOptionsActivity$QRCaptureActivity"
android:screenOrientation="portrait"
android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application>
</manifest>
@@ -0,0 +1,15 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_287_2206)">
<path d="M22.0557 38.25L43.1117 6H1L22.0557 38.25Z" fill="url(#paint0_linear_287_2206)"/>
<path d="M6 28.2444C6.85811 27.3291 8.98625 25.2353 10.6338 24.1827C12.2814 23.13 14.257 20.1209 15.0388 18.7479C17.4224 15.2392 22.7618 7.91286 25.0501 6.67716C25.462 6.35678 26.0608 5.85718 26.3087 5.64745C27.1668 3.7405 30.0844 0.498738 34.8898 2.78706C35.3017 2.64974 36.32 2.61542 36.7777 2.61542C36.4153 2.86334 35.6564 3.58795 35.5191 4.50328C35.153 7.02039 33.7647 8.48874 33.1164 8.90825C32.6587 11.8259 32.0294 14.4002 30.6564 15.3155L31.915 17.5466C33.8029 19.5489 37.7159 23.8737 38.2649 25.1552C36.4344 24.5603 35.2521 23.992 34.8898 23.7822L38.2649 28.416C36.2818 28.2635 31.8235 26.9744 29.8556 23.0385C30.6336 25.1438 31.4001 27.7677 31.6862 28.8165C30.6183 27.9393 28.3224 25.3955 27.6816 22.2376C27.8647 25.304 27.8342 27.4816 27.7961 28.1872C27.2812 27.7105 26.0913 26.2307 25.4505 24.1255V27.6723C24.6821 26.604 23.1363 24.0104 22.9967 22.0533C23.1255 24.2716 23.047 25.3115 22.9906 25.5556L20.0731 22.8097C19.2912 23.2292 17.1898 24.1827 15.0388 24.6403C13.5743 25.876 11.797 28.969 11.0915 30.3611V28.5877L9.14643 30.5327L9.83291 28.4733L8.57433 29.5602C8.28828 29.7318 7.62468 30.0751 7.25857 30.0751C7.39585 29.7547 7.65904 29.4076 7.77345 29.2741L6.11441 29.9034C6.3051 29.3504 6.90388 28.13 7.77345 27.6723C6.58351 28.13 6.09536 28.2444 6 28.2444Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_287_2206" x1="22.0557" y1="38.25" x2="22.0557" y2="-4.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#01D6E6"/>
<stop offset="1" stop-color="#0182E7"/>
</linearGradient>
<clipPath id="clip0_287_2206">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
function pluginRemoteCall(objID, methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
}
function pluginRemoteTest(methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
}
function pluginIsLoggedIn(cb, err) {
fetch("/plugin/isLoggedIn", {
@@ -259,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
.then(x=>x.json())
.then(y=> cb && cb(y));
}
function getDevHttpExchanges(cb) {
fetch("/plugin/getDevHttpExchanges", {
timeout: 1000
})
.then(x=>x.json())
.then(y=> cb && cb(y));
}
function setDevHttpProxy(url, port) {
return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port)
.then(x=>x.json());
}
function sendFakeDevLog(devId, msg) {
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
}
+263 -9
View File
@@ -7,6 +7,9 @@
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
<title>DevPortal</title>
<link rel="icon" type="image/x-icon" href="/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<style>
@@ -150,7 +153,7 @@
.pastPluginUrl {
margin-left: auto;
margin-right: auto;
width: 500px;
width: 700px;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
@@ -160,13 +163,122 @@
box-shadow: 0px 1px 2px #131313;
font-weight: lighter;
cursor: pointer;
position: relative;
}
.pastPluginUrl .deleteButton {
position: absolute;
right: 15px;
height: 100%;
width: 30px;
top: 0px;
padding-top: 2px;
display: grid;
justify-items: center;
align-items: center;
cursor: pointer;
font-weight: 400;
transform: scaleX(1.5);
}
[v-cloak] {
display: none;
}
#cloakLoader {
display: block;
position: absolute;
text-align: center;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background-color: black;
color: white;
padding-top: 50px;
font-family: sans-serif;
}
.httpContainer {
position: relative;
}
.httpLine {
}
.httpLine .request {
height: 50px;
position: relative;
cursor: pointer;
}
.httpLine .request .status {
position: absolute;
left: 10px;
width: 40px;
top: 10px;
padding: 5px;
background-color: #333;
border-radius: 5px;
text-align: center;
}
.httpLine .request .status.error {
background-color: #880000;
}
.httpLine .request .status.success {
background-color: #008800;
}
.httpLine .request .status.warn {
background-color: #803500;
}
.httpLine .request .method {
position: absolute;
left: 55px;
top: 10px;
padding: 5px;
background-color: #333;
border-radius: 5px;
width: 50px;
text-align: center;
}
.httpLine .request .url {
position: absolute;
left: 110px;
top: 10px;
padding: 5px;
background-color: #333;
border-radius: 5px;
}
.httpLine .response {
background-color: #111;
margin-left: 55px;
border-radius: 6px;
padding: 10px;
}
.httpLine .response .body{
white-space: pre-wrap;
font-family: monospace;
background-color: black;
padding: 10px;
}
.httpLine .response .headers {
margin: 10px;
}
.httpLine .response .headers .key {
display: inline-block;
font-weight: bold;
font-size: 14px;
color: #FFF;
}
.httpLine .response .headers .value {
display: inline-block;
font-size: 14px;
color: #AAA;
}
</style>
</head>
<body>
<div id="app">
<v-app>
<v-main>
<div v-cloak id="cloakLoader" v-if="!page">
<h2>Loading..</h2>
First load may take longer
</div>
<v-main v-cloak>
<div id="topMenu">
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
<img src="./dependencies/FutoMainLogo.svg"
@@ -250,10 +362,13 @@
</div>
<div v-if="pastPluginUrls" style="margin-top: 60px;">
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
{{pastPluginUrl}}
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
X
</div>
</div>
</div>
</div>
@@ -385,8 +500,8 @@
</v-card-text>
</v-card>
<div style="width: 50%" v-if="Plugin.currentPlugin">
<!--Get Home-->
<v-card class="requestCard" v-for="req in Testing.requests">
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
<v-card-text>
<div class="title">
<span v-if="req.isOptional">(Optional)</span>
@@ -402,6 +517,11 @@
<div class="code">
{{req.code}}
</div>
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
<a :href="req.docUrl" target="_blank">
Documentation
</a>
</div>
<div>
<div class="parameter" v-for="parameter in req.parameters">
<div class="name">
@@ -416,6 +536,9 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="testSourceRemotely(req)">
Test Android
</v-btn>
<v-btn @click="testSource(req)">
Test
</v-btn>
@@ -497,7 +620,62 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn>Clear</v-btn>
<v-btn @click="Integration.logs = []">Clear</v-btn>
</v-card-actions>
</v-card>
<v-card style="margin: 20px;" v-if="Plugin.currentPlugin && Integration.httpExchanges">
<v-card-title>
Http Logs
</v-card-title>
</v-card-header>
<v-card-text>
<div style="position: absolute; top: 0px; right: 15px;">
<v-checkbox v-model="Integration.showHttpRequests" label="Show Http Requests"></v-checkbox>
</div>
<div class="httpContainer" v-if="Integration.showHttpRequests">
<div class="httpLine" v-for="exchange of Integration.httpExchanges">
<div class="request" @click="toggleHttpExchange(exchange)">
<div :class="[{ success: exchange.response.status < 300, warn: exchange.response.status >= 300 && exchange.response.status < 400, error: exchange.response.status >= 400 }, 'status']">
{{exchange.response.status}}
</div>
<div class="method">
{{exchange.request.method}}
</div>
<div class="url">
{{exchange.request.url}}
</div>
</div>
<div class="response" v-if="exchange.response.show">
<h2>Request Headers</h2>
<div class="headers">
<div class="header" v-for="(headerValue, header) in exchange.request.headers">
<div class="key">
{{header}}
</div>
<div class="value">
{{headerValue}}
</div>
</div>
</div>
<h2>Response</h2>
<div class="headers">
<div class="header" v-for="(headerValue, header) in exchange.response.headers">
<div class="key">
{{header}}
</div>
<div class="value">
{{headerValue}}
</div>
</div>
</div>
<div class="body">{{exchange.response.body}}</div>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn v-if="Integration.showHttpRequests" @click="Integration.httpExchanges = []">Clear</v-btn>
</v-card-actions>
</v-card>
</div>
@@ -535,14 +713,18 @@
<!--<script src="./dependencies/vue.js"></script>-->
<!--<script src="./dependencies/vuetify.js"></script>-->
<script src="./source_docs.js"></script>
<script src="./source_doc_urls.js"></script>
<script src="./source.js"></script>
<script src="./dev_bridge.js"></script>
<script>
IS_TESTING = true;
let lastScriptTag = null;
let shouldDevLog = true;
let shouldLoginCheck = true;
new Vue({
el: '#app',
data: {
searchTestMethods: "",
page: "Plugin",
pastPluginUrls: [],
settings: {},
@@ -550,7 +732,9 @@
lastLogIndex: -1,
lastLogDevID: "",
logs: [],
lastInjectTime: ""
httpExchanges: [],
lastInjectTime: "",
showHttpRequests: false
},
Plugin: {
loadUsingTag: false,
@@ -568,6 +752,9 @@
Testing: {
requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x;
}),
lastResult: "",
@@ -603,7 +790,7 @@
};
setInterval(()=>{
try{
if(!this.Plugin.currentPlugin)
if(!this.Plugin.currentPlugin || !shouldDevLog)
return;
getDevLogs(this.Integration.lastLogIndex, (newLogs)=> {
@@ -631,6 +818,16 @@
});
}
});
if(this.Integration.showHttpRequests) {
getDevHttpExchanges((exchanges)=>{
Vue.nextTick(()=>{
for(i = 0; i < exchanges.length; i++) {
exchanges[i].response.show = false;
this.Integration.httpExchanges.unshift(exchanges[i]);
}
});
});
}
}
catch(ex) {
console.error("Failed update", ex);
@@ -638,7 +835,8 @@
}, 1000);
setInterval(()=>{
try{
this.isTestLoggedIn();
if(shouldLoginCheck)
this.isTestLoggedIn();
}catch(ex){}
}, 2500);
},
@@ -671,6 +869,12 @@
this.reloadPlugin();
});
},
deletePastPlugin(url) {
let currentPastPlugins = this.pastPluginUrls;
currentPastPlugins = currentPastPlugins.filter(x=>x.toLowerCase() != url.toLowerCase());
this.pastPluginUrls = currentPastPlugins;
localStorage.setItem("pastPlugins", JSON.stringify(currentPastPlugins));
},
loginTestPlugin() {
pluginLoginTestPlugin();
setTimeout(()=>{
@@ -857,8 +1061,58 @@
"Error: " + ex;
}
},
testSourceRemotely(req) {
const name = req.title;
const parameterVals = req.parameters.map(x=>{
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
return JSON.parse(x.value.substring(5));
return x.value
});
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;
else
parameterVals.push(__DEV_SETTINGS);
}
const func = source[name];
if(!func)
alert("Test func not found");
try {
const remoteResult = pluginRemoteTest(name, parameterVals);
console.log("Result for " + req.title, remoteResult);
this.Testing.lastResult = "//Results [" + name + "]\n" +
JSON.stringify(remoteResult, null, 3);
this.Testing.lastResultError = "";
}
catch(ex) {
if(ex.plugin_type == "CaptchaRequiredException") {
let shouldCaptcha = confirm("Do you want to request captcha?");
if(shouldCaptcha) {
pluginCaptchaTestPlugin(ex.url, ex.body);
}
}
console.error("Failed to run test for " + req.title, ex);
this.Testing.lastResult = ""
if(ex.message)
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex.message + "\n\n" + ex.stack;
else
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex;
}
},
showTestResults(results) {
},
toggleHttpExchange(exchange) {
exchange.response.show = !exchange.response.show;
},
copyClipboard(cpy) {
if(navigator.clipboard)
+121 -47
View File
@@ -1,13 +1,37 @@
declare class ScriptException extends Error {
//If only one parameter is provided, acts as msg
constructor(type: string, msg: string);
}
declare class TimeoutException extends ScriptException {
declare class LoginRequiredException extends ScriptException {
constructor(msg: string);
}
//Alias
declare class ScriptLoginRequiredException extends ScriptException {
constructor(msg: string);
}
declare class CaptchaRequiredException extends ScriptException {
constructor(url: string, body: string);
}
declare class CriticalException extends ScriptException {
constructor(msg: string);
}
declare class UnavailableException extends ScriptException {
constructor(msg: string);
}
declare class AgeException extends ScriptException {
constructor(msg: string);
}
declare class TimeoutException extends ScriptException {
constructor(msg: string);
}
declare class ScriptImplementationException extends ScriptException {
constructor(msg: string);
}
@@ -38,16 +62,23 @@ declare class FilterCapability {
declare class PlatformAuthorLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
}
declare class PlatformAuthorMembershipLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
}
declare interface PlatformContentDef {
id: PlatformID,
name: string,
thumbnails: Thumbnails,
author: PlatformAuthorLink,
datetime: integer,
url: string
}
declare interface PlatformContent {}
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
contentUrl: string,
contentName: string?,
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
constructor(obj: PlatformNestedMediaContentDef);
}
declare interface PlatformLockedContentDef extends PlatformContentDef {
contentName: string?,
contentThumbnails: Thumbnails?,
unlockUrl: string,
lockDescription: string?,
}
declare class PlatformLockedContent {
constructor(obj: PlatformLockedContentDef);
}
declare interface PlatformVideoDef extends PlatformContentDef {
thumbnails: Thumbnails,
author: PlatformAuthorLink,
duration: int,
viewCount: long,
isLive: boolean
isLive: boolean,
shareUrl: string?
}
declare interface PlatformContent {}
declare class PlatformVideo implements PlatformContent {
constructor(obj: PlatformVideoDef);
}
@@ -77,15 +118,16 @@ declare class PlatformVideo implements PlatformContent {
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
description: string,
video: VideoSourceDescriptor,
live: SubtitleSource[],
rating: IRating
live: IVideoSource,
rating: IRating,
subtitles: SubtitleSource[]
}
declare class PlatformVideoDetails extends PlatformVideo {
constructor(obj: PlatformVideoDetailsDef);
}
declare class PlatformPostDef extends PlatformContentDef {
thumbnails: string[],
declare interface PlatformPostDef extends PlatformContentDef {
thumbnails: Thumbnails[],
images: string[],
description: string
}
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
constructor(obj: PlatformPostDef)
}
declare class PlatformPostDetailsDef extends PlatformPostDef {
declare interface PlatformPostDetailsDef extends PlatformPostDef {
rating: IRating,
textType: int,
content: String
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
isUnMuxed: boolean,
videoSources: VideoSource[]
}
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(obj: VideoSourceDescriptorDef);
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(videoSourcesOrObj: VideoSource[]);
}
declare interface UnMuxVideoSourceDescriptorDef {
@@ -129,7 +171,7 @@ declare interface IVideoSource {
declare interface IAudioSource {
}
interface VideoUrlSourceDef implements IVideoSource {
declare interface VideoUrlSourceDef implements IVideoSource {
width: integer,
height: integer,
container: string,
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
duration: integer,
url: string
}
class VideoUrlSource {
declare class VideoUrlSource {
constructor(obj: VideoUrlSourceDef);
getRequestModifier(): RequestModifier?;
}
interface VideoUrlRangeSourceDef extends VideoUrlSource {
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
itagId: integer,
initStart: integer,
initEnd: integer,
indexStart: integer,
indexEnd: integer,
}
class VideoUrlRangeSource extends VideoUrlSource {
declare class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj: YTVideoSourceDef);
}
interface AudioUrlSourceDef {
declare interface AudioUrlSourceDef {
name: string,
bitrate: integer,
container: string,
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
url: string,
language: string
}
class AudioUrlSource implements IAudioSource {
declare class AudioUrlSource implements IAudioSource {
constructor(obj: AudioUrlSourceDef);
getRequestModifier(): RequestModifier?;
}
interface IRequest {
url: string,
headers: Map<string, string>
}
interface IRequestModifierDef {
allowByteSkip: boolean
}
class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
interface AudioUrlRangeSourceDef extends AudioUrlSource {
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
itagId: integer,
initStart: integer,
initEnd: integer,
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
indexEnd: integer,
audioChannels: integer
}
class AudioUrlRangeSource extends AudioUrlSource {
declare class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj: AudioUrlRangeSourceDef);
}
interface HLSSourceDef {
declare interface HLSSourceDef {
name: string,
duration: integer,
url: string
url: string,
priority: boolean?,
language: string?
}
class HLSSource implements IVideoSource {
declare class HLSSource implements IVideoSource {
constructor(obj: HLSSourceDef);
}
interface DashSourceDef {
declare interface DashSourceDef {
name: string,
duration: integer,
url: string
url: string,
language: string?
}
class DashSource implements IVideoSource {
declare class DashSource implements IVideoSource {
constructor(obj: DashSourceDef)
}
declare interface IRequest {
url: string,
headers: Map<string, string>
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
}
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
//Channel
interface PlatformChannelDef {
declare interface PlatformChannelDef {
id: PlatformID,
name: string,
thumbnail: string,
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
subscribers: integer,
description: string,
url: string,
urlAlternatives: string[],
links: Map<string>?
}
class PlatformChannel {
declare class PlatformChannel {
constructor(obj: PlatformChannelDef);
}
//Playlist
declare interface PlatformPlaylistDef implements PlatformContent {
videoCount: integer,
thumbnail: string
}
declare class PlatformPlaylist extends PlatformContent {
constructor(obj: PlatformPlaylistDef);
}
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
contents: ContentPager
}
declare class PlatformPlaylistDetails extends PlatformContent {
constructor(obj: PlatformPlaylistDetailsDef);
}
//Ratings
interface IRating {
type: integer
@@ -250,7 +313,11 @@ declare class PlatformComment {
constructor(obj: CommentDef);
}
declare class PlaybackTracker {
constructor(interval: integer);
setProgress(seconds: integer);
}
declare class LiveEventPager {
nextRequest = 4000;
@@ -261,8 +328,8 @@ declare class LiveEventPager {
nextPage(): LiveEventPager; //Could be self
}
class LiveEvent {
type: String
declare class LiveEvent {
constructor(type: integer);
}
declare class LiveEventComment extends LiveEvent {
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
@@ -287,25 +354,31 @@ declare class ContentPager {
constructor(results: PlatformContent[], hasMore: boolean);
hasMorePagers(): boolean
nextPage(): VideoPager; //Could be self
nextPage(): ContentPager?; //Could be self
}
declare class VideoPager {
constructor(results: PlatformVideo[], hasMore: boolean);
hasMorePagers(): boolean
nextPage(): VideoPager; //Could be self
nextPage(): VideoPager?; //Could be self
}
declare class ChannelPager {
constructor(results: PlatformChannel[], hasMore: boolean);
hasMorePagers(): boolean;
nextPage(): ChannelPager; //Could be self
nextPage(): ChannelPager?; //Could be self
}
declare class PlaylistPager {
constructor(results: PlatformPlaylist[], hasMore: boolean);
hasMorePagers(): boolean;
nextPage(): PlaylistPager?;
}
declare class CommentPager {
constructor(results: PlatformComment[], hasMore: boolean);
hasMorePagers(): boolean
nextPage(): CommentPager; //Could be self
nextPage(): CommentPager?; //Could be self
}
interface Map<T> {
@@ -341,8 +414,9 @@ interface Source {
getChannelCapabilities(): ResultCapabilities;
isContentDetailsUrl(url: string): boolean;
getContentDetails(url: string): PlatformVideoDetails;
getContentDetails(url: string): PlatformContentDetails;
//Optional
getLiveEvents(url: string): LiveEventPager;
//Optional
+111 -18
View File
@@ -10,7 +10,8 @@ let Type = {
Videos: "VIDEOS",
Streams: "STREAMS",
Mixed: "MIXED",
Live: "LIVE"
Live: "LIVE",
Subscriptions: "SUBSCRIPTIONS"
},
Order: {
Chronological: "CHRONOLOGICAL"
@@ -31,23 +32,31 @@ let Type = {
RAW: 0,
HTML: 1,
MARKUP: 2
},
Chapter: {
NORMAL: 0,
SKIPPABLE: 5,
SKIP: 6,
SKIPONCE: 7
}
};
let Language = {
UNKNOWN: "Unknown",
ARABIC: "Arabic",
SPANISH: "Spanish",
FRENCH: "French",
HINDI: "Hindi",
INDONESIAN: "Indonesian",
KOREAN: "Korean",
PORTBRAZIL: "Portuguese Brazilian",
RUSSIAN: "Russian",
THAI: "Thai",
TURKISH: "Turkish",
VIETNAMESE: "Vietnamese",
ENGLISH: "English"
ARABIC: "ar",
SPANISH: "es",
FRENCH: "fr",
HINDI: "hi",
INDONESIAN: "id",
KOREAN: "ko",
PORTUGUESE: "pt",
PORTBRAZIL: "pt",
RUSSIAN: "ru",
THAI: "th",
TURKISH: "tr",
VIETNAMESE: "vi",
ENGLISH: "en"
}
class ScriptException extends Error {
@@ -64,6 +73,16 @@ class ScriptException extends Error {
}
}
}
class ScriptLoginRequiredException extends ScriptException {
constructor(msg) {
super("ScriptLoginRequiredException", msg);
}
}
class LoginRequiredException extends ScriptException {
constructor(msg) {
super("ScriptLoginRequiredException", msg);
}
}
class CaptchaRequiredException extends Error {
constructor(url, body) {
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
@@ -153,13 +172,27 @@ class FilterCapability {
class PlatformAuthorLink {
constructor(id, name, url, thumbnail, subscribers) {
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
this.id = id ?? PlatformID(); //PlatformID
this.name = name ?? ""; //string
this.url = url ?? ""; //string
this.thumbnail = thumbnail; //string
if(subscribers)
this.subscribers = subscribers;
if(membershipUrl)
this.membershipUrl = membershipUrl ?? null; //string (for backcompat)
}
}
class PlatformAuthorMembershipLink {
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
this.id = id ?? PlatformID(); //PlatformID
this.name = name ?? ""; //string
this.url = url ?? ""; //string
this.thumbnail = thumbnail; //string
if(subscribers)
this.subscribers = subscribers;
if(membershipUrl)
this.membershipUrl = membershipUrl ?? null; //string
}
}
class PlatformContent {
@@ -190,6 +223,16 @@ class PlatformNestedMediaContent extends PlatformContent {
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
}
}
class PlatformLockedContent extends PlatformContent {
constructor(obj) {
super(obj, 70);
obj = obj ?? {};
this.contentName = obj.contentName;
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
this.unlockUrl = obj.unlockUrl ?? "";
this.lockDescription = obj.lockDescription;
}
}
class PlatformVideo extends PlatformContent {
constructor(obj) {
super(obj, 1);
@@ -211,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
this.description = obj.description ?? "";//String
this.video = obj.video ?? {}; //VideoSourceDescriptor
this.dash = obj.dash ?? null; //DashSource
this.hls = obj.hls ?? null; //HLSSource
this.dash = obj.dash ?? null; //DashSource, deprecated
this.hls = obj.hls ?? null; //HLSSource, deprecated
this.live = obj.live ?? null; //VideoSource
this.rating = obj.rating ?? null; //IRating
@@ -283,6 +326,8 @@ class VideoUrlSource {
this.bitrate = obj.bitrate ?? 0;
this.duration = obj.duration ?? 0;
this.url = obj.url;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class VideoUrlRangeSource extends VideoUrlSource {
@@ -308,6 +353,17 @@ class AudioUrlSource {
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class AudioUrlWidevineSource extends AudioUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "AudioUrlWidevineSource";
this.bearerToken = obj.bearerToken;
this.licenseUri = obj.licenseUri;
}
}
class AudioUrlRangeSource extends AudioUrlSource {
@@ -333,6 +389,8 @@ class HLSSource {
this.priority = obj.priority ?? false;
if(obj.language)
this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashSource {
@@ -344,13 +402,48 @@ class DashSource {
this.url = obj.url;
if(obj.language)
this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashManifestRawSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashManifestRawAudioSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawAudioSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
this.manifest = obj.manifest ?? null;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class RequestModifier {
constructor(obj) {
obj = obj ?? {};
this.allowByteSkip = obj.allowByteSkip;
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
}
}
@@ -376,7 +469,7 @@ class PlatformPlaylist extends PlatformContent {
constructor(obj) {
super(obj, 4);
this.plugin_type = "PlatformPlaylist";
this.videoCount = obj.videoCount ?? 0;
this.videoCount = obj.videoCount ?? -1;
this.thumbnail = obj.thumbnail;
}
}
@@ -0,0 +1,114 @@
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) {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
}
}
}
}
}
}
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)
}
}
@@ -1,15 +1,31 @@
package com.futo.platformplayer
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
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.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
val requestExecutor = getRequestExecutor();
return if (requestExecutor != null) {
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
} else if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
}
}
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
File diff suppressed because one or more lines are too long
@@ -1,6 +1,10 @@
package com.futo.platformplayer
import android.util.Log
import com.google.common.base.CharMatcher
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
@@ -165,7 +169,7 @@ private fun parseHextet(ipString: String, start: Int, end: Int): Short {
var hextet = 0
for (i in start until end) {
hextet = hextet shl 4
hextet = hextet or ipString[i].digitToIntOrNull(16)!! ?: -1
hextet = hextet or ipString[i].digitToIntOrNull(16)!!
}
return hextet.toShort()
}
@@ -212,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
}
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000
if (addresses.isEmpty()) {
return null;
}
if (addresses.size == 1) {
val socket = Socket()
try {
return Socket(addresses[0], port);
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
} catch (e: Throwable) {
//Ignored.
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close()
}
return null;
@@ -245,7 +254,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
}
}
socket.connect(InetSocketAddress(address, port));
socket.connect(InetSocketAddress(address, port), timeout);
synchronized(syncObject) {
if (connectedSocket == null) {
@@ -259,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
}
}
} catch (e: Throwable) {
//Ignore
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
}
};
@@ -273,3 +282,46 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
return connectedSocket;
}
fun InputStream.readHttpHeaderBytes() : ByteArray {
val headerBytes = ByteArrayOutputStream()
var crlfCount = 0
while (crlfCount < 4) {
val b = read()
if (b == -1) {
throw IOException("Unexpected end of stream while reading headers")
}
if (b == 0x0D || b == 0x0A) { // CR or LF
crlfCount++
} else {
crlfCount = 0
}
headerBytes.write(b)
}
return headerBytes.toByteArray()
}
fun InputStream.readLine() : String? {
val line = ByteArrayOutputStream()
var crlfCount = 0
while (crlfCount < 2) {
val b = read()
if (b == -1) {
return null
}
if (b == 0x0D || b == 0x0A) { // CR or LF
crlfCount++
} else {
crlfCount = 0
line.write(b)
}
}
return String(line.toByteArray(), Charsets.UTF_8)
}
@@ -1,6 +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 userpackage.Protocol
import kotlin.math.abs
import kotlin.math.min
@@ -35,4 +42,31 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
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)
}
}
@@ -1,5 +1,13 @@
package com.futo.platformplayer
import android.net.Uri
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
//Syntax sugaring
inline fun <reified T> Any.assume(): T?{
if(this is T)
@@ -12,4 +20,26 @@ inline fun <reified T, R> Any.assume(cb: (T) -> R): R? {
if(result != null)
return cb(result);
return null;
}
fun String?.yesNoToBoolean(): Boolean {
return this?.uppercase() == "YES"
}
fun Boolean?.toYesNo(): String {
return if (this == true) "YES" else "NO"
}
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
"[${toString()}]"
}
is Inet4Address -> {
toString()
}
else -> {
throw Exception("Invalid address type")
}
}
}
@@ -27,7 +27,7 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
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}");
return this as T;
return this;
}
//Singles
@@ -109,11 +109,29 @@ inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextN
else
return this.expectOrThrow<V8ValueLong>(config, contextName).value.toLong() as T
};
Float::class -> {
if(this is V8ValueDouble)
return this.value.toFloat() as T;
else if(this is V8ValueInteger)
return this.value.toFloat() as T;
else if(this is V8ValueLong)
return this.value.toFloat() as T;
else
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
};
Double::class -> {
if(this is V8ValueDouble)
return this.value.toDouble() as T;
else if(this is V8ValueInteger)
return this.value.toDouble() as T;
else if(this is V8ValueLong)
return this.value.toDouble() as T;
else
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
};
V8ValueObject::class -> this.expectOrThrow<V8ValueObject>(config, contextName) as T
V8ValueArray::class -> this.expectOrThrow<V8ValueArray>(config, contextName) as T;
Boolean::class -> this.expectOrThrow<V8ValueBoolean>(config, contextName).value as T;
Float::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value.toFloat() as T;
Double::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value as T;
HashMap::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
Map::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
List::class -> this.expectOrThrow<V8ValueArray>(config, contextName).let { V8ArrayToStringList(it) } as T;
@@ -0,0 +1,20 @@
package com.futo.platformplayer
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class HorizontalSpaceItemDecoration(private val startSpace: Int, private val betweenSpace: Int, private val endSpace: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.left = betweenSpace
val position = parent.getChildAdapterPosition(view)
if (position == 0) {
outRect.left = startSpace
}
else if (position == state.itemCount - 1) {
outRect.right = endSpace
}
}
}
@@ -0,0 +1,20 @@
package com.futo.platformplayer
class PresetImages {
companion object {
val images = mapOf<String, Int>(
Pair("xp_book", R.drawable.xp_book),
Pair("xp_forest", R.drawable.xp_forest),
Pair("xp_code", R.drawable.xp_code),
Pair("xp_controller", R.drawable.xp_controller),
Pair("xp_laptop", R.drawable.xp_laptop)
);
fun getPresetResIdByName(name: String): Int {
return images[name] ?: -1;
}
fun getPresetNameByResId(id: Int): String? {
return images.entries.find { it.value == id }?.key;
}
}
}
@@ -2,34 +2,54 @@ 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.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.*
import com.futo.platformplayer.activities.MainActivity
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.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.*
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
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.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.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FormField
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
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
import java.time.OffsetDateTime
@Serializable
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
@@ -43,37 +63,49 @@ class Settings : FragmentedStorageFileJson() {
@Transient
val onTabsChanged = Event0();
@FormField(
"Manage Polycentric identity", FieldForm.BUTTON,
"Manage your Polycentric identity", -3
)
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
@FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let {
if (StatePolycentric.instance.processHandle != null) {
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
if (StatePolycentric.instance.enabled) {
if (StatePolycentric.instance.processHandle != null) {
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
} else {
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
}
} else {
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
UIDialogs.toast(it, "Polycentric is disabled")
}
}
}
@FormField(
"Open FAQ", FieldForm.BUTTON,
"Get answers to common questions", -2
)
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
@FormFieldButton(R.drawable.ic_quiz)
fun openFAQ() {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) {
//Ignored
}
}
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
@FormFieldButton(R.drawable.ic_data_alert)
fun openIssues() {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) {
//Ignored
}
}
/*
@FormField(
"Submit feedback", FieldForm.BUTTON,
"Give feedback on the application", -1
R.string.submit_feedback, FieldForm.BUTTON,
R.string.give_feedback_on_the_application, -1
)
@FormFieldButton(R.drawable.ic_bug)
fun submitFeedback() {
try {
val i = Intent(Intent.ACTION_VIEW);
@@ -87,12 +119,10 @@ class Settings : FragmentedStorageFileJson() {
} catch (e: Throwable) {
//Ignored
}
}
}*/
@FormField(
"Manage Tabs", FieldForm.BUTTON,
"Change tabs visible on the home screen", -1
)
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
@FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() {
try {
SettingsActivity.getActivity()?.let {
@@ -103,11 +133,76 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField("Home", "group", "Configure how your Home tab works and feels", 1)
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
@FormFieldButton(R.drawable.ic_move_up)
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);
}
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
@FormFieldButton(R.drawable.ic_link)
fun manageLinks() {
try {
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show url handling prompt", e)
}
}
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
@FormFieldButton(R.drawable.battery_full_24px)
fun ignoreBatteryOptimization() {
SettingsActivity.getActivity()?.let {
val intent = Intent()
val packageName = it.packageName
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.setData(Uri.parse("package:$packageName"))
it.startActivity(intent)
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
} else {
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
}
}
}*/
@FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings();
@Serializable
class LanguageSettings {
@FormField(R.string.app_language, FieldForm.DROPDOWN, R.string.may_require_restart, 5, "app_language")
@DropdownFieldOptionsId(R.array.app_languages)
var appLanguage: Int = 0;
fun getAppLanguageLocaleString(): String? {
return when(appLanguage) {
0 -> null
1 -> "en";
2 -> "de";
3 -> "es";
4 -> "pt";
5 -> "fr"
6 -> "ja";
7 -> "ko";
8 -> "zh";
9 -> "ru";
10 -> "ar";
else -> null
}
}
}
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
var home = HomeSettings();
@Serializable
class HomeSettings {
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
@DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1;
@@ -117,21 +212,45 @@ class Settings : FragmentedStorageFileJson() {
else
return FeedStyle.THUMBNAIL;
}
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 8)
@FormFieldButton(R.drawable.ic_visibility_off)
fun clearHidden() {
StateMeta.instance.removeAllHiddenCreators();
StateMeta.instance.removeAllHiddenVideos();
SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, "Creators and videos should show up again");
}
}
}
@FormField("Search", "group", "", 2)
@FormField(R.string.search, "group", -1, 2)
var search = SearchSettings();
@Serializable
class SearchSettings {
@FormField("Search History", FieldForm.TOGGLE, "", 4)
@FormField(R.string.search_history, FieldForm.TOGGLE, R.string.may_require_restart, 3)
@Serializable(with = FlexibleBooleanSerializer::class)
var searchHistory: Boolean = true;
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 4)
@DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
fun getSearchFeedStyle(): FeedStyle {
if(searchFeedStyle == 0)
@@ -141,11 +260,21 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField("Subscriptions", "group", "Configure how your Subscriptions works and feels", 3)
@FormField(R.string.channel, "group", -1, 3)
var channel = ChannelSettings();
@Serializable
class ChannelSettings {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
}
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 4)
var subscriptions = SubscriptionsSettings();
@Serializable
class SubscriptionsSettings {
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 4)
@DropdownFieldOptionsId(R.array.feed_style)
var subscriptionsFeedStyle: Int = 1;
@@ -156,11 +285,23 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL;
}
@FormField("Fetch on app boot", FieldForm.TOGGLE, "Shortly after opening the app, start fetching subscriptions.", 6)
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
@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;
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 7)
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
@DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0;
@@ -176,26 +317,67 @@ class Settings : FragmentedStorageFileJson() {
};
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 8)
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
@DropdownFieldOptionsId(R.array.thread_count)
var subscriptionConcurrency: Int = 3;
fun getSubscriptionsConcurrency() : Int {
return threadIndexToCount(subscriptionConcurrency);
}
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear();
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
}
}
@FormField("Player", "group", "Change behavior of the player", 4)
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 5)
var playback = PlaybackSettings();
@Serializable
class PlaybackSettings {
@FormField("Primary Language", FieldForm.DROPDOWN, "", 0)
@DropdownFieldOptionsId(R.array.languages)
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
@DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0;
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.languages)[primaryLanguage];
fun getPrimaryLanguage(context: Context): String? {
return when(primaryLanguage) {
0 -> "en";
1 -> "es";
2 -> "de";
3 -> "fr";
4 -> "ja";
5 -> "ko";
6 -> "th";
7 -> "vi";
8 -> "id";
9 -> "hi";
10 -> "ar";
11 -> "tu";
12 -> "ru";
13 -> "pt";
14 -> "zh";
else -> null
}
}
@FormField("Default Playback Speed", FieldForm.DROPDOWN, "", 1)
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.playback_speeds)
var defaultPlaybackSpeed: Int = 3;
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
@@ -211,51 +393,43 @@ class Settings : FragmentedStorageFileJson() {
else -> 1.0f;
};
@FormField("Preferred Quality", FieldForm.DROPDOWN, "", 2)
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredQuality: Int = 0;
@FormField("Preferred Metered Quality", FieldForm.DROPDOWN, "", 2)
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredMeteredQuality: Int = 0;
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
@FormField("Preferred Preview Quality", FieldForm.DROPDOWN, "", 3)
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@FormField("Auto-Rotate", FieldForm.DROPDOWN, "", 4)
@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;
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
fun isAutoRotate() = (autoRotate == 1 && !StatePlayer.instance.rotationLock) || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate() && !StatePlayer.instance.rotationLock);
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5)
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
var autoRotateDeadZone: Int = 0;
fun getAutoRotateDeadZoneDegrees(): Int {
return autoRotateDeadZone * 5;
}
@FormField("Background Behavior", FieldForm.DROPDOWN, "", 6)
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2;
fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
@FormField("Resume After Preview", FieldForm.DROPDOWN, "When watching a video in preview mode, resume at the position when opening the video", 7)
@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;
@FormField("Live Chat Webview", FieldForm.TOGGLE, "Use the live chat web window when available over native implementation.", 8)
var useLiveChatWindow: Boolean = true;
fun shouldResumePreview(previewedPosition: Long): Boolean{
if(resumeAfterPreview == 2)
return true;
@@ -263,14 +437,69 @@ class Settings : FragmentedStorageFileJson() {
return true;
return false;
}
@FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 8)
@DropdownFieldOptionsId(R.array.chapter_fps)
var chapterUpdateFPS: Int = 0;
fun getChapterUpdateFrames(): Int {
return when(chapterUpdateFPS) {
0 -> 24
1 -> 30
2 -> 60
3 -> 120
else -> 1
};
}
@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;
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterConnectivityLoss: Int = 1;
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 14)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 15)
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
@FormFieldWarning(R.string.changing_this_field_requires_restart)
var allowVideoToGoUnderCutout: Boolean = true;
}
@FormField("Downloads", "group", "Configure downloading of videos", 5)
@FormField(R.string.comments, "group", R.string.comments_description, 6)
var comments = CommentSettings();
@Serializable
class CommentSettings {
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 0;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
}
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
var downloads = Downloads();
@Serializable
class Downloads {
@FormField("Download when", FieldForm.DROPDOWN, "Configure when videos should be downloaded", 0)
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_videos_should_be_downloaded, 0)
@DropdownFieldOptionsId(R.array.when_download)
var whenDownload: Int = 0;
@@ -283,21 +512,21 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField("Default Video Quality", FieldForm.DROPDOWN, "", 2)
@FormField(R.string.default_video_quality, FieldForm.DROPDOWN, -1, 2)
@DropdownFieldOptionsId(R.array.preferred_video_download)
var preferredVideoQuality: Int = 4;
fun getDefaultVideoQualityPixels(): Int = preferedQualityToPixels(preferredVideoQuality);
@FormField("Default Audio Quality", FieldForm.DROPDOWN, "", 3)
@FormField(R.string.default_audio_quality, FieldForm.DROPDOWN, -1, 3)
@DropdownFieldOptionsId(R.array.preferred_audio_download)
var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@FormField("ByteRange Download", FieldForm.TOGGLE, "Attempt to utilize byte ranges, this can be combined with concurrency to bypass throttling", 4)
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true;
@FormField("ByteRange Concurrency", FieldForm.DROPDOWN, "Number of concurrent threads to multiply download speeds from throttled sources", 5)
@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;
fun getByteRangeThreadCount(): Int {
@@ -305,23 +534,30 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField("Browsing", "group", "Configure browsing behavior", 6)
@FormField(R.string.browsing, "group", R.string.configure_browsing_behavior, 8)
var browsing = Browsing();
@Serializable
class Browsing {
@FormField("Enable Video Cache", FieldForm.TOGGLE, "A cache to quickly load previously fetched videos", 0)
@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;
}
@FormField("Casting", "group", "Configure casting", 7)
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
var casting = Casting();
@Serializable
class Casting {
@FormField("Enabled", FieldForm.TOGGLE, "Enable casting", 0)
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enable_casting, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var enabled: Boolean = true;
@FormField(R.string.keep_screen_on, FieldForm.TOGGLE, R.string.keep_screen_on_while_casting, 1)
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@@ -339,25 +575,23 @@ class Settings : FragmentedStorageFileJson() {
}*/
}
@FormField("Logging", FieldForm.GROUP, "", 8)
@FormField(R.string.logging, FieldForm.GROUP, -1, 10)
var logging = Logging();
@Serializable
class Logging {
@FormField("Log Level", FieldForm.DROPDOWN, "", 0)
@FormField(R.string.log_level, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.log_levels)
var logLevel: Int = 0;
@FormField(
"Submit logs", FieldForm.BUTTON,
"Submit logs to help us narrow down issues", 1
)
fun isVerbose() = logLevel >= 4;
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
fun submitLogs() {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
if (!Logger.submitLogs()) {
withContext(Dispatchers.Main) {
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Please enable logging to submit logs") }
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
}
}
} catch (e: Throwable) {
@@ -367,43 +601,40 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField("Announcement", FieldForm.GROUP, "", 10)
@FormField(R.string.announcement, FieldForm.GROUP, -1, 11)
var announcementSettings = AnnouncementSettings();
@Serializable
class AnnouncementSettings {
@FormField(
"Reset announcements", FieldForm.BUTTON,
"Reset hidden announcements", 1
)
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
fun resetAnnouncements() {
StateAnnouncement.instance.resetAnnouncements();
UIDialogs.toast("Announcements reset.");
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
}
}
@FormField("Plugins", FieldForm.GROUP, "", 11)
@FormField(R.string.notifications, FieldForm.GROUP, -1, 12)
var notifications = NotificationSettings();
@Serializable
class NotificationSettings {
@FormField(R.string.planned_content_notifications, FieldForm.TOGGLE, R.string.planned_content_notifications_description, 1)
var plannedContentNotification: Boolean = true;
}
@FormField(R.string.plugins, FieldForm.GROUP, -1, 13)
@Transient
var plugins = Plugins();
@Serializable
class Plugins {
@FormField("Clear Cookies on Logout", FieldForm.TOGGLE, "Clears cookies when you log out, allowing you to change account.", 0)
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true;
@FormField(
"Clear Cookies", FieldForm.BUTTON,
"Clears in-app browser cookies, especially useful for fully logging out of plugins.", 1
)
@FormField(R.string.clear_cookies, FieldForm.BUTTON, R.string.clears_in_app_browser_cookies, 1)
fun clearCookies() {
val cookieManager: CookieManager = CookieManager.getInstance();
cookieManager.removeAllCookies(null);
}
@FormField(
"Reinstall Embedded Plugins", FieldForm.BUTTON,
"Also removes any data related plugin like login or settings (may not clear browser cache)", 1
)
/*@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
fun reinstallEmbedded() {
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
try {
@@ -411,7 +642,7 @@ class Settings : FragmentedStorageFileJson() {
withContext(Dispatchers.Main) {
StateApp.instance.contextOrNull?.let {
UIDialogs.toast(it, "Embedded plugins reinstalled, a reboot is recommended");
UIDialogs.toast(it, it.getString(R.string.embedded_plugins_reinstalled_a_reboot_is_recommended));
};
}
} catch (ex: Exception) {
@@ -422,11 +653,11 @@ class Settings : FragmentedStorageFileJson() {
}
}
}
}
}*/
}
@FormField("External Storage", FieldForm.GROUP, "", 12)
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 14)
var storage = Storage();
@Serializable
class Storage {
@@ -438,34 +669,41 @@ class Settings : FragmentedStorageFileJson() {
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
@FormField("Change external General directory", FieldForm.BUTTON, "Change the external directory for general files, used for persistent files like auto-backup", 3)
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
fun changeStorageGeneral() {
SettingsActivity.getActivity()?.let {
StateApp.instance.changeExternalGeneralDirectory(it);
}
}
@FormField("Change external Downloads directory", FieldForm.BUTTON, "Change the external storage for download files, used for exported download files", 4)
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
fun changeStorageDownload() {
SettingsActivity.getActivity()?.let {
StateApp.instance.changeExternalDownloadDirectory(it);
}
}
@FormField(R.string.clear_external_downloads_directory, FieldForm.BUTTON, R.string.clear_the_external_storage_for_download_files, 5)
fun clearStorageDownload() {
Settings.instance.storage.storage_download = null;
Settings.instance.save();
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
}
}
@FormField("Auto Update", "group", "Configure the auto updater", 12)
@FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 15)
var autoUpdate = AutoUpdate();
@Serializable
class AutoUpdate {
@FormField("Check", FieldForm.DROPDOWN, "", 0)
@FormField(R.string.check, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.auto_update_when_array)
var check: Int = 0;
@FormField("Background download", FieldForm.DROPDOWN, "Configure if background download should be used", 1)
@FormField(R.string.background_download, FieldForm.DROPDOWN, R.string.configure_if_background_download_should_be_used, 1)
@DropdownFieldOptionsId(R.array.background_download)
var backgroundDownload: Int = 0;
@FormField("Download when", FieldForm.DROPDOWN, "Configure when updates should be downloaded", 2)
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
@DropdownFieldOptionsId(R.array.when_download)
var whenDownload: Int = 0;
@@ -482,33 +720,30 @@ class Settings : FragmentedStorageFileJson() {
return check == 0 && !BuildConfig.IS_PLAYSTORE_BUILD;
}
@FormField(
"Manual check", FieldForm.BUTTON,
"Manually check for updates", 3
)
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
fun manualCheck() {
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
SettingsActivity.getActivity()?.let {
StateUpdate.instance.checkForUpdates(it, true);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(it, true)
}
}
} else {
SettingsActivity.getActivity()?.let {
try {
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
} catch (e: ActivityNotFoundException) {
UIDialogs.toast(it, "Failed to show store.");
UIDialogs.toast(it, it.getString(R.string.failed_to_show_store));
}
}
}
}
@FormField(
"View changelog", FieldForm.BUTTON,
"Review the current and past changelogs", 4
)
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
fun viewChangelog() {
UIDialogs.toast("Retrieving changelog");
SettingsActivity.getActivity()?.let {
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
@@ -524,10 +759,7 @@ class Settings : FragmentedStorageFileJson() {
};
}
@FormField(
"Remove Cached Version", FieldForm.BUTTON,
"Remove the last downloaded version", 5
)
@FormField(R.string.remove_cached_version, FieldForm.BUTTON, R.string.remove_the_last_downloaded_version, 5)
fun removeCachedVersion() {
StateApp.withContext {
val outputDirectory = File(it.filesDir, "autoupdate");
@@ -543,7 +775,7 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField("Backup", FieldForm.GROUP, "", 13)
@FormField(R.string.backup, FieldForm.GROUP, -1, 16)
var backup = Backup();
@Serializable
class Backup {
@@ -553,58 +785,107 @@ class Settings : FragmentedStorageFileJson() {
var autoBackupPassword: String? = null;
fun shouldAutomaticBackup() = autoBackupPassword != null;
@FormField("Automatic Backup", FieldForm.READONLYTEXT, "", 0)
@FormField(R.string.automatic_backup, FieldForm.READONLYTEXT, -1, 0)
val automaticBackupText get() = if(!shouldAutomaticBackup()) "None" else "Every Day";
@FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 1)
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
fun configureAutomaticBackup() {
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
SettingsActivity.getActivity()?.reloadSettings();
};
}
@FormField("Restore Automatic Backup", FieldForm.BUTTON, "Restore a previous automatic backup", 2)
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
fun restoreAutomaticBackup() {
val activity = SettingsActivity.getActivity()!!
if(!StateBackup.hasAutomaticBackup())
UIDialogs.toast(activity, "You don't have any automatic backups", false);
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
else
UIDialogs.showAutomaticRestoreDialog(activity, activity.lifecycleScope);
}
@FormField("Export Data", FieldForm.BUTTON, "Creates a zip file with your data which can be imported by opening it with Grayjay", 3)
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
fun export() {
StateBackup.startExternalBackup();
val activity = SettingsActivity.getActivity() ?: return;
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
StateBackup.shareExternalBackup();
}),
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
StateBackup.saveExternalBackup(activity);
})
)
}
}
@FormField("Payment", FieldForm.GROUP, "", 14)
@FormField(R.string.payment, FieldForm.GROUP, -1, 17)
var payment = Payment();
@Serializable
class Payment {
@FormField("Payment Status", FieldForm.READONLYTEXT, "", 1)
val paymentStatus: String get() = if (StatePayment.instance.hasPaid) "Paid" else "Not Paid";
@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("Clear Payment", FieldForm.BUTTON, "Deletes license keys from app", 2)
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun clearPayment() {
StatePayment.instance.clearLicenses();
SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, "Licenses cleared, might require app restart");
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
it.reloadSettings();
}
}
}
@FormField("Info", FieldForm.GROUP, "", 15)
@FormField(R.string.other, FieldForm.GROUP, -1, 18)
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;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
var polycentricEnabled: Boolean = true;
}
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
var gestureControls = GestureControls();
@Serializable
class GestureControls {
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
var volumeSlider: Boolean = true;
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
var brightnessSlider: Boolean = true;
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
var toggleFullscreen: Boolean = true;
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
var useSystemBrightness: Boolean = false;
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
var useSystemVolume: Boolean = true;
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
var restoreSystemBrightness: Boolean = true;
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
var zoom: Boolean = true;
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
var pan: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
var info = Info();
@Serializable
class Info {
@FormField("Version Code", FieldForm.READONLYTEXT, "", 1, "code")
@FormField(R.string.version_code, FieldForm.READONLYTEXT, -1, 1, "code")
var versionCode = BuildConfig.VERSION_CODE;
@FormField("Version Name", FieldForm.READONLYTEXT, "", 2)
@FormField(R.string.version_name, FieldForm.READONLYTEXT, -1, 2)
var versionName = BuildConfig.VERSION_NAME;
@FormField("Version Type", FieldForm.READONLYTEXT, "", 3)
@FormField(R.string.version_type, FieldForm.READONLYTEXT, -1, 3)
var versionType = BuildConfig.BUILD_TYPE;
}
@@ -615,6 +896,7 @@ class Settings : FragmentedStorageFileJson() {
companion object {
private const val TAG = "Settings";
const val URL_FAQ = "https://grayjay.app/faq.html";
private var _isFirst = true;
@@ -2,53 +2,71 @@ package com.futo.platformplayer
import android.content.Context
import android.webkit.CookieManager
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.fields.ButtonField
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.stream.IntStream.range
import kotlin.system.measureTimeMillis
@Serializable()
class SettingsDev : FragmentedStorageFileJson() {
@FormField("Developer Mode", FieldForm.TOGGLE, "", 0)
@FormField(R.string.developer_mode, FieldForm.TOGGLE, -1, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var developerMode: Boolean = false;
@FormField("Development Server", FieldForm.GROUP,
"Settings related to development server, be careful as it may open your phone to security vulnerabilities", 1)
@FormField(R.string.development_server, FieldForm.GROUP,
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 1)
val devServerSettings: DeveloperServerFields = DeveloperServerFields();
@Serializable
class DeveloperServerFields {
@FormField("Start Server on boot", FieldForm.TOGGLE, "", 0)
@FormField(R.string.start_server_on_boot, FieldForm.TOGGLE, -1, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var devServerOnBoot: Boolean = false;
@FormField("Start Server", FieldForm.BUTTON,
"Starts a DevServer on port 11337, may expose vulnerabilities.", 1)
@FormField(R.string.start_server, FieldForm.BUTTON,
R.string.starts_a_devServer_on_port_11337_may_expose_vulnerabilities, 1)
fun startServer() {
StateDeveloper.instance.runServer();
StateApp.instance.contextOrNull?.let {
@@ -57,45 +75,192 @@ class SettingsDev : FragmentedStorageFileJson() {
}
}
@FormField("Experimental", FieldForm.GROUP,
"Settings related to development server, be careful as it may open your phone to security vulnerabilities", 2)
@FormField(R.string.experimental, FieldForm.GROUP,
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 2)
val experimentalSettings: ExperimentalFields = ExperimentalFields();
@Serializable
class ExperimentalFields {
@FormField("Background Subscription Testing", FieldForm.TOGGLE, "", 0)
@FormField(R.string.background_subscription_testing, FieldForm.TOGGLE, -1, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var backgroundSubscriptionFetching: Boolean = false;
}
@FormField("Crash Me", FieldForm.BUTTON,
"Crashes the application on purpose", 2)
@FormField(R.string.cache, FieldForm.GROUP, -1, 3)
val cache: Cache = Cache();
@Serializable
class Cache {
@FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1, "subscription_cache_button")
fun subscriptionsCache5000() {
Logger.i("SettingsDev", "Started caching 5000 sub items");
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Started caching 5000 sub items"
);
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
if(button is ButtonField)
button.setButtonEnabled(false);
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val subsCache =
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this).first;
var total = 0;
var page = 0;
var lastToast = System.currentTimeMillis();
while(subsCache.hasMorePages() && total < 5000) {
subsCache.nextPage();
total += subsCache.getResults().size;
page++;
if(page % 10 == 0)
withContext(Dispatchers.Main) {
val diff = System.currentTimeMillis() - lastToast;
lastToast = System.currentTimeMillis();
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
);
}
Thread.sleep(250);
}
withContext(Dispatchers.Main) {
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"FINISHED Page: ${page}, Total: ${total}"
);
}
}
catch(ex: Throwable) {
Logger.e("SettingsDev", ex.message, ex);
Logger.i("SettingsDev", "Failed: ${ex.message}");
}
finally {
withContext(Dispatchers.Main) {
if(button is ButtonField)
button.setButtonEnabled(true);
}
}
}
}
@FormField(R.string.history_cache_100, FieldForm.BUTTON, -1, 1, "history_cache_button")
fun historyCache100() {
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Started caching 100 history items (from home)"
);
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
if(button is ButtonField)
button.setButtonEnabled(false);
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val subsCache = StatePlatform.instance.getHome();
var num = 0;
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
num++;
}
var total = 0;
var page = 0;
var lastToast = System.currentTimeMillis();
while(subsCache.hasMorePages() && total < 5000) {
subsCache.nextPage();
total += subsCache.getResults().size;
page++;
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
num++;
}
if(page % 4 == 0)
withContext(Dispatchers.Main) {
val diff = System.currentTimeMillis() - lastToast;
lastToast = System.currentTimeMillis();
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
);
}
Thread.sleep(500);
}
withContext(Dispatchers.Main) {
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"FINISHED Page: ${page}, Total: ${total}"
);
}
}
catch(ex: Throwable) {
Logger.e("SettingsDev", ex.message, ex);
Logger.i("SettingsDev", "Failed: ${ex.message}");
}
finally {
withContext(Dispatchers.Main) {
if(button is ButtonField)
button.setButtonEnabled(true);
}
}
}
}
}
@FormField(R.string.crash_me, FieldForm.BUTTON,
R.string.crashes_the_application_on_purpose, 3)
fun crashMe() {
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
}
@FormField("Delete Announcements", FieldForm.BUTTON,
"Delete all announcements", 2)
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
R.string.delete_all_announcements, 3)
fun deleteAnnouncements() {
StateAnnouncement.instance.deleteAllAnnouncements();
}
@FormField("Clear Cookies", FieldForm.BUTTON,
"Clear all cook from the CookieManager", 2)
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
R.string.clear_all_cookies_from_the_cookieManager, 3)
fun clearCookies() {
val cookieManager: CookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null);
}
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
R.string.test_background_worker_description, 4)
fun triggerBackgroundUpdate() {
val act = SettingsActivity.getActivity()!!;
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
val wm = WorkManager.getInstance(act);
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
.build();
wm.enqueue(req);
}
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 4)
fun clearChannelContentCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
StateCache.instance.clearToday();
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
}
@Contextual
@Transient
@FormField("V8 Benchmarks", FieldForm.GROUP,
"Various benchmarks using the integrated V8 engine", 3)
@FormField(R.string.v8_benchmarks, FieldForm.GROUP,
R.string.various_benchmarks_using_the_integrated_v8_engine, 4)
val v8Benchmarks: V8Benchmarks = V8Benchmarks();
class V8Benchmarks {
@FormField(
"Test V8 Creation speed", FieldForm.BUTTON,
"Tests V8 creation times and running", 1
R.string.test_v8_creation_speed, FieldForm.BUTTON,
R.string.tests_v8_creation_times_and_running, 1
)
fun testV8Creation() {
var plugin: V8Plugin? = null;
@@ -137,8 +302,8 @@ class SettingsDev : FragmentedStorageFileJson() {
}
@FormField(
"Test V8 Communication speed", FieldForm.BUTTON,
"Tests V8 communication speeds", 2
R.string.test_v8_communication_speed, FieldForm.BUTTON,
R.string.tests_v8_communication_speeds, 4
)
fun testV8RunSpeeds() {
var plugin: V8Plugin? = null;
@@ -182,12 +347,12 @@ class SettingsDev : FragmentedStorageFileJson() {
@Contextual
@Transient
@FormField("V8 Script Testing", FieldForm.GROUP, "Various tests against a custom source", 4)
@FormField(R.string.v8_script_testing, FieldForm.GROUP, R.string.various_tests_against_a_custom_source, 4)
val v8ScriptTests: V8ScriptTests = V8ScriptTests();
class V8ScriptTests {
@Contextual
private var _currentPlugin : JSClient? = null;
@FormField("Inject", FieldForm.BUTTON, "Injects a test source config (local) into V8", 1)
@FormField(R.string.inject, FieldForm.BUTTON, R.string.injects_a_test_source_config_local_into_v8, 1)
fun testV8Init() {
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
@@ -203,12 +368,12 @@ class SettingsDev : FragmentedStorageFileJson() {
}
}
}
@FormField("getHome", FieldForm.BUTTON, "Attempts to fetch 2 pages from getHome", 2)
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
fun testV8Home() {
runTestPlugin(_currentPlugin) {
var home: IPager<IPlatformContent>? = null;
var resultPage1: String = "";
var resultPage2: String = "";
var home: IPager<IPlatformContent>?;
val resultPage1: String;
val resultPage2: String;
val page1Time = measureTimeMillis {
home = it.getHome();
val results = home!!.getResults();
@@ -269,27 +434,36 @@ class SettingsDev : FragmentedStorageFileJson() {
@Contextual
@Transient
@FormField("Other", FieldForm.GROUP, "Others...", 5)
@FormField(R.string.other, FieldForm.GROUP, R.string.others_ellipsis, 5)
val otherTests: OtherTests = OtherTests();
class OtherTests {
@FormField("Clear Downloads", FieldForm.BUTTON, "Deletes all ongoing downloads", 1)
@FormField(R.string.unsubscribe_all, FieldForm.BUTTON, R.string.removes_all_subscriptions, -1)
fun unsubscribeAll() {
val toUnsub = StateSubscriptions.instance.getSubscriptions();
UIDialogs.toast("Started unsubbing.. (${toUnsub.size})")
toUnsub.forEach {
StateSubscriptions.instance.removeSubscription(it.channel.url);
};
UIDialogs.toast("Finished unsubbing.. (${toUnsub.size})")
}
@FormField(R.string.clear_downloads, FieldForm.BUTTON, R.string.deletes_all_ongoing_downloads, 1)
fun clearDownloads() {
StateDownloads.instance.getDownloading().forEach {
StateDownloads.instance.removeDownload(it);
};
}
@FormField("Clear All Downloaded", FieldForm.BUTTON, "Deletes all downloaded videos and related files", 2)
@FormField(R.string.clear_all_downloaded, FieldForm.BUTTON, R.string.deletes_all_downloaded_videos_and_related_files, 2)
fun clearDownloaded() {
StateDownloads.instance.getDownloadedVideos().forEach {
StateDownloads.instance.deleteCachedVideo(it.id);
};
}
@FormField("Delete Unresolved", FieldForm.BUTTON, "Deletes all unresolved source files", 3)
@FormField(R.string.delete_unresolved, FieldForm.BUTTON, R.string.deletes_all_unresolved_source_files, 3)
fun cleanupDownloads() {
StateDownloads.instance.cleanupDownloads();
}
@FormField("Fill storage till error", FieldForm.BUTTON, "Writes to disk till no space is left", 4)
@FormField(R.string.fill_storage_till_error, FieldForm.BUTTON, R.string.writes_to_disk_till_no_space_is_left, 4)
fun fillStorage(context: Context, scope: CoroutineScope?) {
val gigabuffer = ByteArray(1024 * 1024 * 128);
var count: Long = 0;
@@ -318,8 +492,39 @@ class SettingsDev : FragmentedStorageFileJson() {
}
}
}
@FormField(R.string.test_playback, FieldForm.BUTTON,
R.string.test_playback, 1)
fun testPlayback(context: Context) {
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
}
}
@FormField(R.string.networking, FieldForm.GROUP, -1, 18)
var networking = Networking();
@Serializable
class Networking {
@FormField(R.string.allow_all_certificates, FieldForm.TOGGLE, -1, 0)
@FormFieldWarning(R.string.allow_all_certificates_warning)
var allowAllCertificates: Boolean = false;
}
@Contextual
@Transient
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
var info = Info();
@Serializable
class Info {
@FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize")
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
}
//region BOILERPLATE
override fun encode(): String {
return Json.encodeToString(this);
@@ -0,0 +1,55 @@
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 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) {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
}
}
}
}
init {
orientationListener.enable()
lastOrientation = activity.resources.configuration.orientation
}
fun stopListening() {
orientationListener.disable()
}
}
@@ -1,24 +1,50 @@
package com.futo.platformplayer
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.dialogs.*
import com.futo.platformplayer.dialogs.AutoUpdateDialog
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
import com.futo.platformplayer.dialogs.AutomaticRestoreDialog
import com.futo.platformplayer.dialogs.CastingAddDialog
import com.futo.platformplayer.dialogs.CastingHelpDialog
import com.futo.platformplayer.dialogs.ChangelogDialog
import com.futo.platformplayer.dialogs.CommentDialog
import com.futo.platformplayer.dialogs.ConnectCastingDialog
import com.futo.platformplayer.dialogs.ConnectedCastingDialog
import com.futo.platformplayer.dialogs.ImportDialog
import com.futo.platformplayer.dialogs.ImportOptionsDialog
import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.PluginUpdateDialog
import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -91,6 +117,50 @@ class UIDialogs {
}.toTypedArray());
}
fun showUrlHandlingPrompt(context: Context, onYes: (() -> Unit)? = null) {
val builder = AlertDialog.Builder(context)
val view = LayoutInflater.from(context).inflate(R.layout.dialog_url_handling, null)
builder.setView(view)
val dialog = builder.create()
registerDialogOpened(dialog)
view.findViewById<TextView>(R.id.button_no).apply {
this.setOnClickListener {
dialog.dismiss()
}
}
view.findViewById<LinearLayout>(R.id.button_yes).apply {
this.setOnClickListener {
if (BuildConfig.IS_PLAYSTORE_BUILD) {
dialog.dismiss()
showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.play_store_version_does_not_support_default_url_handling)) {
onYes?.invoke()
}
} else {
try {
val intent =
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
context.startActivity(intent)
} catch (e: Throwable) {
toast(context, context.getString(R.string.failed_to_show_settings))
}
onYes?.invoke()
dialog.dismiss()
}
}
}
dialog.setOnDismissListener {
registerDialogClosed(dialog)
}
dialog.show()
}
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
val dialogAction: ()->Unit = {
@@ -100,14 +170,15 @@ class UIDialogs {
dialog.show();
};
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
UIDialogs.showDialog(context, R.drawable.ic_move_up, "An old backup is available", "Would you like to restore this backup?", null, 0,
UIDialogs.Action("Cancel", {}), //To nothing
UIDialogs.Action("Override", {
UIDialogs.showDialog(context, R.drawable.ic_move_up, context.getString(R.string.an_old_backup_is_available), context.getString(R.string.would_you_like_to_restore_this_backup), null, 0,
UIDialogs.Action(context.getString(R.string.cancel), {}), //To nothing
UIDialogs.Action(context.getString(R.string.override), {
dialogAction();
}, UIDialogs.ActionStyle.DANGEROUS),
UIDialogs.Action("Restore", {
UIDialogs.Action(context.getString(R.string.restore), {
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
}, UIDialogs.ActionStyle.PRIMARY));
}, UIDialogs.ActionStyle.PRIMARY)
);
else {
dialogAction();
}
@@ -119,6 +190,14 @@ class UIDialogs {
dialog.show();
}
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
@@ -142,8 +221,10 @@ class UIDialogs {
view.findViewById<TextView>(R.id.dialog_text_code).apply {
if(code == null)
this.visibility = View.GONE;
else
else {
this.text = code;
this.visibility = View.VISIBLE;
}
};
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
val buttons = actions.map<Action, TextView> { act ->
@@ -202,22 +283,48 @@ class UIDialogs {
}, UIDialogs.ActionStyle.PRIMARY)
);
}
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
val pluginConfig = if(ex is PluginException) ex.config else null;
val pluginInfo = if(ex is PluginException)
"\nPlugin [${ex.config.name}]" else "";
showDialog(context,
R.drawable.ic_error_pred,
"${msg}${pluginInfo}",
(if(ex != null ) "${ex.message}" else ""),
if(ex is PluginException) ex.code else null,
0,
UIDialogs.Action("Retry", {
retryAction?.invoke();
}, UIDialogs.ActionStyle.PRIMARY),
UIDialogs.Action("Close", {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE)
);
var exMsg = if(ex != null ) "${ex.message}" else "";
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
exMsg += "\n\nAn update is available"
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
showDialog(context,
R.drawable.ic_error_pred,
"${msg}${pluginInfo}",
exMsg,
if(ex is PluginException) ex.code else null,
1,
UIDialogs.Action(context.getString(R.string.update), {
mainFragment.navigate<SourceDetailFragment>(SourceDetailFragment.UpdatePluginAction(pluginConfig));
if(mainFragment is VideoDetailFragment)
mainFragment.minimizeVideoDetail();
}, UIDialogs.ActionStyle.ACCENT),
UIDialogs.Action(context.getString(R.string.close), {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
retryAction?.invoke();
}, UIDialogs.ActionStyle.PRIMARY)
);
else
showDialog(context,
R.drawable.ic_error_pred,
"${msg}${pluginInfo}",
exMsg,
if(ex is PluginException) ex.code else null,
0,
UIDialogs.Action(context.getString(R.string.close), {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
retryAction?.invoke();
}, UIDialogs.ActionStyle.PRIMARY)
);
}
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
@@ -226,24 +333,28 @@ class UIDialogs {
}
fun showDataRetryDialog(context: Context, reason: String? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
val retryButtonAction = Action("Retry", retryAction ?: {}, ActionStyle.PRIMARY)
val closeButtonAction = Action("Close", closeAction ?: {}, ActionStyle.ACCENT)
showDialog(context, R.drawable.ic_no_internet_86dp, "Data Retry", reason, null, 0, closeButtonAction, retryButtonAction)
val retryButtonAction = Action(context.getString(R.string.retry), retryAction ?: {}, ActionStyle.PRIMARY)
val closeButtonAction = Action(context.getString(R.string.close), closeAction ?: {}, ActionStyle.ACCENT)
showDialog(context, R.drawable.ic_no_internet_86dp, context.getString(R.string.data_retry), reason, null, 0, closeButtonAction, retryButtonAction)
}
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
val confirmButtonAction = Action("Confirm", action, ActionStyle.PRIMARY)
val cancelButtonAction = Action("Cancel", cancelAction ?: {}, ActionStyle.ACCENT)
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
}
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
dialog.setMaxVersion(lastVersion);
if (hideExceptionButtons) {
dialog.hideExceptionButtons()
}
}
fun showChangelogDialog(context: Context, lastVersion: Int) {
@@ -273,8 +384,14 @@ class UIDialogs {
}
}
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showImportOptionsDialog(context: MainActivity) {
val dialog = ImportOptionsDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
@@ -285,17 +402,34 @@ class UIDialogs {
val d = StateCasting.instance.activeDevice;
if (d != null) {
val dialog = ConnectedCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
} else {
val dialog = ConnectCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
val c = context
if (c is Activity) {
dialog.setOwnerActivity(c);
}
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
}
fun showCastingTutorialDialog(context: Context) {
val dialog = CastingHelpDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showCastingAddDialog(context: Context) {
val dialog = CastingAddDialog(context);
registerDialogOpened(dialog);
@@ -310,13 +444,28 @@ class UIDialogs {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try {
StateApp.withContext {
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
toast(it, text, long);
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e);
}
}
}
fun appToast(text: String, long: Boolean = false) {
appToast(ToastView.Toast(text, long))
}
fun appToastError(text: String, long: Boolean) {
StateApp.withContext {
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
};
}
fun appToast(toast: ToastView.Toast) {
StateApp.withContext {
if(it is MainActivity) {
it.showAppToast(toast);
}
}
}
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
//TODO: Is not actually clickable...
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,7 @@ import android.os.OperationCanceledException
import android.util.TypedValue
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
@@ -68,6 +69,12 @@ fun ensureNotMainThread() {
}
}
private val _regexUrl = Regex("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)");
fun String.isHttpUrl(): Boolean {
return _regexUrl.matchEntire(this) != null;
}
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
fun String.isHexColor(): Boolean {
return _regexHexColor.matches(this);
@@ -137,8 +144,11 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
}
}
@Suppress("DEPRECATION")
fun Activity.setNavigationBarColorAndIcons() {
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
if (Settings.instance.playback.allowVideoToGoUnderCutout)
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
@@ -158,9 +168,7 @@ fun Int.sp(resources: Resources): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt()
}
fun File.share(context: Context) {
val uri = FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), this);
fun DocumentFile.share(context: Context) {
val shareIntent = Intent();
shareIntent.action = Intent.ACTION_SEND;
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -1,16 +1,25 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.view.View
import android.widget.*
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
@@ -29,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var _sourceHeader: SourceHeaderView;
private lateinit var _sourcePermissions: LinearLayout;
private lateinit var _sourceWarnings: LinearLayout;
private lateinit var _sourceWarningsContainer: LinearLayout;
private lateinit var _container: ScrollView;
private lateinit var _loader: ImageView;
@@ -45,6 +56,10 @@ class AddSourceActivity : AppCompatActivity() {
private var _config: SourcePluginConfig? = null;
private var _script: String? = null;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
@@ -67,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
_sourcePermissions = findViewById(R.id.source_permissions);
_sourceWarnings = findViewById(R.id.source_warnings);
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
_container = findViewById(R.id.configContainer);
_loader = findViewById(R.id.loader);
@@ -96,8 +112,8 @@ class AddSourceActivity : AppCompatActivity() {
var url = intent?.dataString;
if(url == null)
UIDialogs.showDialog(this, R.drawable.ic_error, "No valid URL provided..", null, null,
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
else {
if(url.startsWith("vfuto://"))
url = "https://" + url.substring("vfuto://".length);
@@ -129,14 +145,14 @@ class AddSourceActivity : AppCompatActivity() {
Logger.e(TAG, "Failed decode config", ex);
withContext(Dispatchers.Main) {
UIDialogs.showDialog(this@AddSourceActivity, R.drawable.ic_error,
"Invalid Config Format", null, null,
getString(R.string.invalid_config_format), null, null,
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
};
return@launch;
} catch(ex: Exception) {
Logger.e(TAG, "Failed fetch config", ex);
withContext(Dispatchers.Main) {
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, "Failed to fetch configuration", ex);
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_configuration), ex);
};
return@launch;
}
@@ -152,7 +168,7 @@ class AddSourceActivity : AppCompatActivity() {
} catch (ex: Exception) {
Logger.e(TAG, "Failed fetch script", ex);
withContext(Dispatchers.Main) {
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, "Failed to fetch script", ex);
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_script), ex);
};
return@launch;
}
@@ -175,8 +191,8 @@ class AddSourceActivity : AppCompatActivity() {
_sourcePermissions.addView(
SourceInfoView(this,
R.drawable.ic_language,
"URL Access",
"The plugin will have access to the following domains",
getString(R.string.url_access),
getString(R.string.the_plugin_will_have_access_to_the_following_domains),
config.allowUrls, true)
)
@@ -184,28 +200,37 @@ class AddSourceActivity : AppCompatActivity() {
_sourcePermissions.addView(
SourceInfoView(this,
R.drawable.ic_code,
"Eval Access",
"The plugin will have access to eval capability (remote injection)",
getString(R.string.eval_access),
getString(R.string.the_plugin_will_have_access_to_eval_capability_remote_injection),
config.allowUrls, true)
)
val pastelRed = resources.getColor(R.color.pastel_red);
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
for(warning in config.getWarnings(script))
val warnings = config.getWarnings(script);
for(warning in warnings)
_sourceWarnings.addView(
SourceInfoView(this,
R.drawable.ic_security_pred,
warning.first,
warning.second)
.withDescriptionColor(pastelRed));
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
setLoading(false);
}
fun install(config: SourcePluginConfig, script: String) {
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
if(it)
if(it) {
StatePlugins.instance.clearUpdateAvailable(config)
if(isNew)
lifecycleScope.launch {
StatePlatform.instance.enableClient(listOf(config.id));
}
backToSources();
}
}
}
@@ -1,5 +1,6 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -7,22 +8,24 @@ import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton;
lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton;
lateinit var _buttonPlugins: BigButton;
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
scanResult?.let {
val content = it.contents
if (content == null) {
UIDialogs.toast(this, "Failed to scan QR code")
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
return@let
}
@@ -31,7 +34,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
} else if (content.startsWith("grayjay://plugin/")) {
content.substring("grayjay://plugin/".length)
} else {
UIDialogs.toast(this, "Not a plugin URL")
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
return@let;
}
@@ -42,6 +45,10 @@ class AddSourceOptionsActivity : AppCompatActivity() {
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_source_options);
@@ -50,7 +57,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
_buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr);
_buttonBrowse = findViewById(R.id.option_browse);
_buttonURL = findViewById(R.id.option_url);
_buttonPlugins = findViewById(R.id.option_plugins);
_buttonBack.setOnClickListener {
finish();
@@ -59,7 +68,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
_buttonQR.onClick.subscribe {
val integrator = IntentIntegrator(this);
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt("Scan a QR Code")
integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true);
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
@@ -67,14 +76,12 @@ class AddSourceOptionsActivity : AppCompatActivity() {
integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent())
}
_buttonBrowse.onClick.subscribe {
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
}
_buttonURL.onClick.subscribe {
UIDialogs.toast(this, "Not implemented yet..");
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
}
}
class QRCaptureActivity: CaptureActivity() {
}
}
@@ -18,6 +18,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.CaptchaWebViewClient
import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
@@ -31,6 +32,10 @@ class CaptchaActivity : AppCompatActivity() {
private lateinit var _webView: WebView;
private lateinit var _buttonClose: Button;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_captcha);
@@ -1,17 +1,24 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.IField
class DeveloperActivity : AppCompatActivity() {
private lateinit var _form: FieldForm;
private lateinit var _buttonBack: ImageButton;
fun getField(id: String): IField? {
return _form.findField(id);
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
DeveloperActivity._lastActivity = this;
setContentView(R.layout.activity_dev);
setNavigationBarColorAndIcons();
@@ -19,7 +26,7 @@ class DeveloperActivity : AppCompatActivity() {
_form = findViewById(R.id.settings_form);
_form.fromObject(SettingsDev.instance);
_form.onChanged.subscribe { field, value ->
_form.onChanged.subscribe { _, _ ->
_form.setObjectValues();
SettingsDev.instance.save();
};
@@ -33,4 +40,19 @@ class DeveloperActivity : AppCompatActivity() {
super.finish()
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
private var _lastActivity: DeveloperActivity? = null;
fun getActivity(): DeveloperActivity? {
val act = _lastActivity;
if(act != null)
return act;
return null;
}
}
}
@@ -1,16 +1,22 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logging
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -24,9 +30,14 @@ class ExceptionActivity : AppCompatActivity() {
private lateinit var _buttonSubmit: LinearLayout;
private lateinit var _buttonRestart: LinearLayout;
private lateinit var _buttonClose: LinearLayout;
private lateinit var _buttonCheckForUpdates: LinearLayout;
private var _file: File? = null;
private var _submitted = false;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exception);
@@ -37,9 +48,10 @@ class ExceptionActivity : AppCompatActivity() {
_buttonSubmit = findViewById(R.id.button_submit);
_buttonRestart = findViewById(R.id.button_restart);
_buttonClose = findViewById(R.id.button_close);
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?";
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
@@ -75,17 +87,28 @@ class ExceptionActivity : AppCompatActivity() {
_buttonClose.setOnClickListener {
finish();
};
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
_buttonCheckForUpdates.visibility = View.VISIBLE
_buttonCheckForUpdates.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
}
}
} else {
_buttonCheckForUpdates.visibility = View.GONE
}
}
private fun submitFile() {
if (_submitted) {
Toast.makeText(this, "Logs already submitted.", Toast.LENGTH_LONG).show();
Toast.makeText(this, getString(R.string.logs_already_submitted), Toast.LENGTH_LONG).show();
return;
}
val file = _file;
if (file == null) {
Toast.makeText(this, "No logs found.", Toast.LENGTH_LONG).show();
Toast.makeText(this, getString(R.string.no_logs_found), Toast.LENGTH_LONG).show();
return;
}
@@ -101,14 +124,14 @@ class ExceptionActivity : AppCompatActivity() {
withContext(Dispatchers.Main) {
if (id == null) {
try {
Toast.makeText(this@ExceptionActivity, "Failed automated share, share manually?", Toast.LENGTH_LONG).show();
Toast.makeText(this@ExceptionActivity, getString(R.string.failed_automated_share_share_manually), Toast.LENGTH_LONG).show();
} catch (e: Throwable) {
//Ignored
}
} else {
_submitted = true;
file.delete();
Toast.makeText(this@ExceptionActivity, "Shared $id", Toast.LENGTH_LONG).show();
Toast.makeText(this@ExceptionActivity, getString(R.string.shared_id).replace("{id}", id), Toast.LENGTH_LONG).show();
}
}
}
@@ -119,10 +142,10 @@ class ExceptionActivity : AppCompatActivity() {
val i = Intent(Intent.ACTION_SEND);
i.type = "text/plain";
i.putExtra(Intent.EXTRA_EMAIL, arrayOf("grayjay@futo.org"));
i.putExtra(Intent.EXTRA_SUBJECT, "Unhandled exception in VS");
i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.unhandled_exception_in_vs));
i.putExtra(Intent.EXTRA_TEXT, exceptionString);
startActivity(Intent.createChooser(i, "Send exception to developers..."));
startActivity(Intent.createChooser(i, getString(R.string.send_exception_to_developers)));
} catch (e: Throwable) {
//Ignored
@@ -0,0 +1,108 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dialogs.CastingHelpDialog
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
class FCastGuideActivity : AppCompatActivity() {
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fcast_guide);
setNavigationBarColorAndIcons();
findViewById<TextView>(R.id.text_explanation).apply {
val guideText = """
<h3>1. Install FCast Receiver:</h3>
<p>- Open Play Store, FireStore, or FCast website on your TV/desktop.<br>
- Search for "FCast Receiver", install and open it.</p>
<br>
<h3>2. Prepare the Grayjay App:</h3>
<p>- Ensure it's connected to the same network as the FCast Receiver.</p>
<br>
<h3>3. Initiate Casting from Grayjay:</h3>
<p>- Click the cast button in Grayjay.</p>
<br>
<h3>4. Connect to FCast Receiver:</h3>
<p>- Wait for your device to show in the list or add it manually with its IP address.</p>
<br>
<h3>5. Confirm Connection:</h3>
<p>- Click "OK" to confirm your device selection.</p>
<br>
<h3>6. Start Casting:</h3>
<p>- Press "start" next to the device you've added.</p>
<br>
<h3>7. Play Your Video:</h3>
<p>- Start any video in Grayjay to cast.</p>
<br>
<h3>Finding Your IP Address:</h3>
<p><b>On FCast Receiver (Android):</b> Displayed on the main screen.<br>
<b>On Windows:</b> Use 'ipconfig' in Command Prompt.<br>
<b>On Linux:</b> Use 'hostname -I' or 'ip addr' in Terminal.<br>
<b>On MacOS:</b> System Preferences > Network.</p>
""".trimIndent()
text = Html.fromHtml(guideText, Html.FROM_HTML_MODE_COMPACT)
}
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
UIDialogs.showCastingTutorialDialog(this)
finish()
}
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
UIDialogs.showCastingTutorialDialog(this)
finish()
}
findViewById<BigButton>(R.id.button_website).onClick.subscribe {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
startActivity(browserIntent);
} catch (e: Throwable) {
Logger.i(TAG, "Failed to open browser.", e)
}
}
findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
startActivity(browserIntent);
} catch (e: Throwable) {
Logger.i(TAG, "Failed to open browser.", e)
}
}
}
override fun onBackPressed() {
UIDialogs.showCastingTutorialDialog(this)
finish()
}
companion object {
private const val TAG = "FCastGuideActivity";
}
}
@@ -2,7 +2,6 @@ package com.futo.platformplayer.activities
import android.content.Intent
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
interface IWithResultLauncher {
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
@@ -3,33 +3,48 @@ package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.webkit.ConsoleMessage
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
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.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class LoginActivity : AppCompatActivity() {
private lateinit var _webView: WebView;
private lateinit var _textUrl: TextView;
private lateinit var _buttonClose: ImageButton;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
setNavigationBarColorAndIcons();
_textUrl = findViewById(R.id.text_url);
_buttonClose = findViewById(R.id.button_close);
_buttonClose.setOnClickListener {
UIDialogs.toast("Login cancelled", false);
finish();
}
_webView = findViewById(R.id.web_view);
_webView.settings.javaScriptEnabled = true;
CookieManager.getInstance().setAcceptCookie(true);
@@ -60,6 +75,8 @@ class LoginActivity : AppCompatActivity() {
};
var isFirstLoad = true;
webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: "");
if(!isFirstLoad)
return@subscribe;
isFirstLoad = false;
@@ -85,7 +102,7 @@ class LoginActivity : AppCompatActivity() {
override fun finish() {
lifecycleScope.launch(Dispatchers.Main) {
_webView?.loadUrl("about:blank");
_webView.loadUrl("about:blank");
}
_callback?.let {
_callback = null;
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
@@ -10,6 +11,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.ItemMoveCallback
@@ -23,6 +25,10 @@ class ManageTabsActivity : AppCompatActivity() {
private lateinit var _recyclerTabs: RecyclerView;
private lateinit var _touchHelper: ItemTouchHelper;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_manage_tabs);
@@ -49,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
Settings.instance.save()
}
val items = Settings.instance.tabs.mapNotNull {
val items = ArrayList(Settings.instance.tabs.mapNotNull {
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
TabViewHolderData(buttonDefinition, it.enabled)
};
});
_listTabs = _recyclerTabs.asAny(items) {
it.onDragDrop.subscribe { vh ->
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.View
@@ -16,9 +17,15 @@ import androidx.appcompat.app.AppCompatActivity
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.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.*
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.StorageTypeCRDTItem
import com.futo.polycentric.core.StorageTypeCRDTSetItem
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
@@ -33,6 +40,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_backup);
@@ -53,22 +64,19 @@ class PolycentricBackupActivity : AppCompatActivity() {
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
_imageQR.setImageBitmap(qrCodeBitmap);
} catch (e: Exception) {
Logger.e(TAG, "Failed to generate QR code", e);
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
_imageQR.visibility = View.INVISIBLE;
_textQR.visibility = View.INVISIBLE;
}
_buttonShare.onClick.subscribe {
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain";
putExtra(Intent.EXTRA_TEXT, _exportBundle);
}
startActivity(Intent.createChooser(shareIntent, "Share Text"));
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
startActivity(Intent.createChooser(shareIntent, "Share ID"));
};
_buttonCopy.onClick.subscribe {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
val clip = ClipData.newPlainText("Copied Text", _exportBundle);
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
clipboard.setPrimaryClip(clip);
};
}
@@ -1,5 +1,6 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
@@ -9,14 +10,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.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -28,6 +31,10 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
private var _creating = false;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_create_profile);
@@ -54,7 +61,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
try {
val username = _profileName.text.toString();
if (username.length < 3) {
UIDialogs.toast(this@PolycentricCreateProfileActivity, "Must be at least 3 characters long.");
UIDialogs.toast(this@PolycentricCreateProfileActivity, getString(R.string.must_be_at_least_3_characters_long));
return@setOnClickListener;
}
@@ -64,20 +71,29 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
try {
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
processHandle.addServer("https://srv1-stg.polycentric.io");
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to create profile .", e);
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
try {
processHandle.fullyBackfillServers();
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to fully backfill servers.");
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
}
withContext(Dispatchers.Main) {
@@ -1,5 +1,6 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
@@ -15,6 +16,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.dp
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.polycentric.core.Store
@@ -27,6 +29,10 @@ class PolycentricHomeActivity : AppCompatActivity() {
private lateinit var _buttonImportProfile: BigButton;
private lateinit var _layoutButtons: LinearLayout;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_home);
@@ -47,7 +53,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
this.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt());
};
profileButton.withPrimaryText(systemState.username);
profileButton.withSecondaryText("Sign in to this identity");
profileButton.withSecondaryText(getString(R.string.sign_in_to_this_identity));
profileButton.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(processHandle);
startActivity(Intent(this@PolycentricHomeActivity, PolycentricProfileActivity::class.java));
@@ -1,5 +1,6 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
@@ -11,13 +12,20 @@ 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.polycentric.core.*
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.base64UrlToByteArray
import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
@@ -28,6 +36,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonScanProfile: LinearLayout;
private lateinit var _buttonImportProfile: LinearLayout;
private lateinit var _editProfile: EditText;
private lateinit var _loaderOverlay: LoaderOverlay;
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@@ -39,6 +48,10 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_import_profile);
@@ -47,6 +60,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help);
_buttonScanProfile = findViewById(R.id.button_scan_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile);
_loaderOverlay = findViewById(R.id.loader_overlay);
_editProfile = findViewById(R.id.edit_profile);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
@@ -59,7 +73,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonScanProfile.setOnClickListener {
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt("Scan a QR code")
integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true);
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
@@ -70,7 +84,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonImportProfile.setOnClickListener {
if (_editProfile.text.isEmpty()) {
UIDialogs.toast(this, "Text field does not contain any data");
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
return@setOnClickListener;
}
@@ -85,54 +99,71 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
private fun import(url: String) {
if (!url.startsWith("polycentric://")) {
UIDialogs.toast(this, "Not a valid URL");
UIDialogs.toast(this, getString(R.string.not_a_valid_url));
return;
}
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
_loaderOverlay.show()
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
lifecycleScope.launch(Dispatchers.IO) {
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
if (existingProcessSecret != null) {
UIDialogs.toast(this, "This profile is already imported");
return;
}
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
if (existingProcessSecret != null) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
}
return@launch;
}
val processHandle = processSecret.toProcessHandle();
val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
for (e in exportBundle.events.eventsList) {
try {
val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e);
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) {
try {
val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
} catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e);
}
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
}
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide();
}
}
StatePolycentric.instance.setProcessHandle(processHandle);
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
UIDialogs.toast(this, "Failed to import profile: '${e.message}'");
}
}
companion object {
private const val TAG = "PolycentricImportProfileActivity";
}
class QRCaptureActivity: CaptureActivity() {
}
}
@@ -1,7 +1,10 @@
package com.futo.platformplayer.activities
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@@ -11,23 +14,27 @@ import android.webkit.MimeTypeMap
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
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.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.Store
import com.futo.polycentric.core.Synchronization
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.toURLInfoDataLink
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -44,8 +51,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String;
private lateinit var _imagePolycentric: ImageView;
private lateinit var _loaderOverlay: LoaderOverlay;
private lateinit var _textSystem: TextView;
private var _avatarUri: Uri? = null;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_profile);
@@ -57,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
_buttonExport = findViewById(R.id.button_export);
_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<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired();
finish();
};
lifecycleScope.launch(Dispatchers.IO) {
try {
val processHandle = StatePolycentric.instance.processHandle!!;
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to backfill client");
}
}
}
updateUI();
_imagePolycentric.setOnClickListener {
ImagePicker.with(this)
.cropSquare()
@@ -101,10 +99,10 @@ class PolycentricProfileActivity : AppCompatActivity() {
}
_buttonDelete.onClick.subscribe {
UIDialogs.showConfirmationDialog(this, "Are you sure you want to remove this profile?", {
UIDialogs.showConfirmationDialog(this, getString(R.string.are_you_sure_you_want_to_remove_this_profile), {
val processHandle = StatePolycentric.instance.processHandle;
if (processHandle == null) {
UIDialogs.toast(this, "No process handle set");
UIDialogs.toast(this, getString(R.string.no_process_handle_set));
return@showConfirmationDialog;
}
@@ -114,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
finish();
});
}
_textSystem.setOnLongClickListener {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
clipboard.setPrimaryClip(clip)
return@setOnLongClickListener true
}
updateUI()
StatePolycentric.instance.processHandle?.let { processHandle ->
_loaderOverlay.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide()
}
}
}
}
}
private fun saveIfRequired() {
@@ -122,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
var hasChanges = false;
val username = _editName.text.toString();
if (username.length < 3) {
UIDialogs.toast(this@PolycentricProfileActivity, "Name must be at least 3 characters long");
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
}
return@launch;
}
val processHandle = StatePolycentric.instance.processHandle;
if (processHandle == null) {
UIDialogs.toast(this@PolycentricProfileActivity, "Process handle unset");
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
}
return@launch;
}
@@ -143,7 +176,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
val bytes = readBytesFromUri(applicationContext.contentResolver, avatarUri);
if (bytes == null) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to read image");
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_read_image));
}
return@launch;
@@ -186,14 +219,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
if (hasChanges) {
try {
processHandle.fullyBackfillServers();
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, "Changes have been saved");
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
}
} catch (e: Throwable) {
Logger.w(TAG, "Failed to synchronize changes", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to synchronize changes");
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_synchronize_changes));
}
}
}
@@ -211,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
private fun updateUI() {
val processHandle = StatePolycentric.instance.processHandle!!;
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
_textSystem.text = processHandle.system.key.toBase64Url()
_username = systemState.username;
_editName.text.clear();
_editName.text.append(_username);
@@ -219,7 +255,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
Glide.with(_imagePolycentric)
.load(avatar?.toURLInfoDataLink(processHandle.system.toProto(), processHandle.processSecret.process.toProto(), systemState.servers.toList()))
.load(avatar?.toURLInfoSystemLinkUrl(processHandle.system.toProto(), avatar.process, systemState.servers.toList()))
.placeholder(R.drawable.placeholder_profile)
.crossfade()
.into(_imagePolycentric)
@@ -235,12 +271,12 @@ class PolycentricProfileActivity : AppCompatActivity() {
} else if (resultCode == ImagePicker.RESULT_ERROR) {
UIDialogs.toast(this, ImagePicker.getError(data));
} else {
UIDialogs.toast(this, "Image picker cancelled");
UIDialogs.toast(this, getString(R.string.image_picker_cancelled));
}
}
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
var mimeType: String? = null;
var mimeType: String?;
// Try to get MIME type from the content URI
mimeType = contentResolver.getType(uri);
@@ -1,5 +1,6 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -7,12 +8,17 @@ import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
class PolycentricWhyActivity : AppCompatActivity() {
private lateinit var _buttonVideo: BigButton;
private lateinit var _buttonTechnical: BigButton;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_why);
@@ -0,0 +1,7 @@
package com.futo.platformplayer.activities
import com.journeyapps.barcodescanner.CaptureActivity
class QRCaptureActivity : CaptureActivity() {
}
@@ -1,19 +1,27 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.Loader
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.ReadOnlyTextField
import com.google.android.material.button.MaterialButton
@@ -21,13 +29,27 @@ import com.google.android.material.button.MaterialButton
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
private lateinit var _form: FieldForm;
private lateinit var _buttonBack: ImageButton;
private lateinit var _loader: Loader;
private lateinit var _loaderView: LoaderView;
private lateinit var _devSets: LinearLayout;
private lateinit var _buttonDev: MaterialButton;
private var _isFinished = false;
lateinit var overlay: FrameLayout;
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
@@ -37,12 +59,45 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
_buttonBack = findViewById(R.id.button_back);
_buttonDev = findViewById(R.id.button_dev);
_devSets = findViewById(R.id.dev_settings);
_loader = findViewById(R.id.loader);
_loaderView = findViewById(R.id.loader);
overlay = findViewById(R.id.overlay_container);
_form.onChanged.subscribe { field, value ->
_form.onChanged.subscribe { field, _ ->
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
_form.setObjectValues();
Settings.instance.save();
if(field.descriptor?.id == "app_language") {
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
}
if(field.descriptor?.id == "background_update") {
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if(!notifManager.areNotificationsEnabled()) {
UIDialogs.toast(this, "Notifications aren't enabled");
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
"Notifications need to be enabled for background updating to function", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
}
}
};
_buttonBack.setOnClickListener {
finish();
@@ -57,10 +112,15 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
reloadSettings();
}
var isFirstLoad = true;
fun reloadSettings() {
_loader.start();
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false);
_loaderView.start();
_form.fromObject(lifecycleScope, Settings.instance) {
_loader.stop();
_loaderView.stop();
_form.setSearchVisible(true);
var devCounter = 0;
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
@@ -70,9 +130,16 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
SettingsDev.instance.developerMode = true;
SettingsDev.instance.save();
updateDevMode();
UIDialogs.toast(this, "You are now in developer mode");
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
}
};
if(firstLoad) {
val query = intent.getStringExtra("query");
if(!query.isNullOrEmpty()) {
_form.setSearchQuery(query);
}
}
};
}
@@ -118,11 +185,19 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent);
}
override fun onDestroy() {
super.onDestroy()
settingsActivityClosed.emit()
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
private var _lastActivity: SettingsActivity? = null;
val settingsActivityClosed = Event0()
fun getActivity(): SettingsActivity? {
val act = _lastActivity;
if(act != null && !act._isFinished)
@@ -1,5 +1,7 @@
package com.futo.platformplayer.api.http
import androidx.collection.arrayMapOf
import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
@@ -13,12 +15,16 @@ import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.Dictionary
import java.util.concurrent.TimeUnit
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.time.Duration
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import kotlin.system.measureTimeMillis
open class ManagedHttpClient {
protected val _builderTemplate: OkHttpClient.Builder;
protected var _builderTemplate: OkHttpClient.Builder;
private var client: OkHttpClient;
@@ -27,8 +33,38 @@ open class ManagedHttpClient {
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
fun setTimeout(timeout: Long) {
rebuildClient {
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
.connectTimeout(Duration.ofMillis(timeout));
}
}
private val trustAllCerts = arrayOf<TrustManager>(
object: X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf();
}
}
);
private fun trustAllCertificates(builder: OkHttpClient.Builder) {
val sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, SecureRandom());
builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager);
builder.hostnameVerifier { a, b ->
return@hostnameVerifier true;
}
Logger.w(TAG, "Creating INSECURE client (TrustAll)");
}
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
@@ -36,6 +72,15 @@ open class ManagedHttpClient {
}.build();
}
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
_builderTemplate = modify(_builderTemplate);
client = _builderTemplate.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
return@addNetworkInterceptor response;
}.build();
}
open fun clone(): ManagedHttpClient {
val clonedClient = ManagedHttpClient(_builderTemplate);
clonedClient.user_agent = user_agent;
@@ -60,7 +105,7 @@ open class ManagedHttpClient {
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url);
if(user_agent != null && !user_agent.isEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
requestBuilder.addHeader("User-Agent", user_agent)
for (pair in headers.entries)
@@ -137,7 +182,7 @@ open class ManagedHttpClient {
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(request.method, requestBody)
.url(request.url);
if(user_agent != null && !user_agent.isEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
if(user_agent.isNotEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
requestBuilder.addHeader("User-Agent", user_agent)
for (pair in request.headers.entries)
@@ -148,7 +193,7 @@ open class ManagedHttpClient {
val time = measureTimeMillis {
val call = client.newCall(requestBuilder.build());
request.onCallCreated?.emit(call);
request.onCallCreated.emit(call);
response = call.execute()
resp = Response(
response.code,
@@ -8,16 +8,20 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.StringWriter
import java.net.SocketTimeoutException
class HttpContext : AutoCloseable {
private val _stream: BufferedReader;
private val _inputStream: InputStream;
private var _responseStream: OutputStream? = null;
var id: String? = null;
var head: String = "";
var headers: HttpHeaders = HttpHeaders();
@@ -39,76 +43,130 @@ class HttpContext : AutoCloseable {
private val _responseHeaders: HttpHeaders = HttpHeaders();
constructor(stream: BufferedReader, responseStream: OutputStream? = null, requestId: String? = null, timeout: Int? = null) {
_stream = stream;
constructor(inputStream: InputStream, responseStream: OutputStream? = null, requestId: String? = null, timeout: Int? = null) {
_inputStream = inputStream;
_responseStream = responseStream;
this.id = requestId;
try {
head = stream.readLine() ?: throw EmptyRequestException("No head found");
}
catch(ex: SocketTimeoutException) {
if((timeout ?: 0) > 0)
throw KeepAliveTimeoutException("Keep-Alive timedout", ex);
throw ex;
}
val methodEndIndex = head.indexOf(' ');
val urlEndIndex = head.indexOf(' ', methodEndIndex + 1);
if (methodEndIndex == -1 || urlEndIndex == -1) {
Logger.w(TAG, "Skipped request, wrong format.");
throw IllegalStateException("Invalid request");
}
method = head.substring(0, methodEndIndex);
path = head.substring(methodEndIndex + 1, urlEndIndex);
if (path.contains("?")) {
val queryPartIndex = path.indexOf("?");
val queryParts = path.substring(queryPartIndex + 1).split("&");
path = path.substring(0, queryPartIndex);
for(queryPart in queryParts) {
val eqIndex = queryPart.indexOf("=");
if(eqIndex > 0)
query.put(queryPart.substring(0, eqIndex), queryPart.substring(eqIndex + 1));
else
query.put(queryPart, "");
val headerBytes = readHeaderBytes()
ByteArrayInputStream(headerBytes).use {
val reader = it.bufferedReader(Charsets.UTF_8)
try {
head = reader.readLine() ?: throw EmptyRequestException("No head found");
}
catch(ex: SocketTimeoutException) {
if((timeout ?: 0) > 0)
throw KeepAliveTimeoutException("Keep-Alive timedout", ex);
throw ex;
}
}
while (true) {
val line = stream.readLine();
val headerEndIndex = line.indexOf(":");
if (headerEndIndex == -1)
break;
val methodEndIndex = head.indexOf(' ');
val urlEndIndex = head.indexOf(' ', methodEndIndex + 1);
if (methodEndIndex == -1 || urlEndIndex == -1) {
Logger.w(TAG, "Skipped request, wrong format.");
throw IllegalStateException("Invalid request");
}
val headerKey = line.substring(0, headerEndIndex).lowercase()
val headerValue = line.substring(headerEndIndex + 1).trim();
headers[headerKey] = headerValue;
method = head.substring(0, methodEndIndex);
path = head.substring(methodEndIndex + 1, urlEndIndex);
when(headerKey) {
"content-length" -> contentLength = headerValue.toLong();
"content-type" -> contentType = headerValue;
"connection" -> keepAlive = headerValue.lowercase() == "keep-alive";
"keep-alive" -> {
val keepAliveParams = headerValue.split(",");
for(keepAliveParam in keepAliveParams) {
val eqIndex = keepAliveParam.indexOf("=");
if(eqIndex > 0){
when(keepAliveParam.substring(0, eqIndex)) {
"timeout" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
"max" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
if (path.contains("?")) {
val queryPartIndex = path.indexOf("?");
val queryParts = path.substring(queryPartIndex + 1).split("&");
path = path.substring(0, queryPartIndex);
for(queryPart in queryParts) {
val eqIndex = queryPart.indexOf("=");
if(eqIndex > 0)
query.put(queryPart.substring(0, eqIndex), queryPart.substring(eqIndex + 1));
else
query.put(queryPart, "");
}
}
while (true) {
val line = reader.readLine();
val headerEndIndex = line.indexOf(":");
if (headerEndIndex == -1)
break;
val headerKey = line.substring(0, headerEndIndex).lowercase()
val headerValue = line.substring(headerEndIndex + 1).trim();
headers[headerKey] = headerValue;
when(headerKey) {
"content-length" -> contentLength = headerValue.toLong();
"content-type" -> contentType = headerValue;
"connection" -> keepAlive = headerValue.lowercase() == "keep-alive";
"keep-alive" -> {
val keepAliveParams = headerValue.split(",");
for(keepAliveParam in keepAliveParams) {
val eqIndex = keepAliveParam.indexOf("=");
if(eqIndex > 0){
when(keepAliveParam.substring(0, eqIndex)) {
"timeout" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
"max" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
}
}
}
}
}
if(line.isNullOrEmpty())
break;
}
if(line.isNullOrEmpty())
break;
}
}
private fun readHeaderBytes(): ByteArray {
val headerBytes = ByteArrayOutputStream()
var crlfCount = 0
while (crlfCount < 4) {
val b = _inputStream.read()
if (b == -1) {
throw IOException("Unexpected end of stream while reading headers")
}
if (b == 0x0D || b == 0x0A) { // CR or LF
crlfCount++
} else {
crlfCount = 0
}
headerBytes.write(b)
}
return headerBytes.toByteArray()
}
fun readContentBytes(buffer: ByteArray, length: Int): Int {
val remainingBytes = (contentLength - _totalRead).coerceAtMost(length.toLong()).toInt()
val read = _inputStream.read(buffer, 0, remainingBytes);
if (read > 0) {
_totalRead += read
}
return read;
}
fun readContentString(): String {
val byteArrayOutputStream = ByteArrayOutputStream()
val buffer = ByteArray(4096)
var read: Int
while (true) {
read = readContentBytes(buffer, buffer.size)
if (read <= 0) break
byteArrayOutputStream.write(buffer, 0, read)
}
return byteArrayOutputStream.toString(Charsets.UTF_8.name())
}
inline fun <reified T> readContentJson() : T {
return Serializer.json.decodeFromString(readContentString());
}
fun skipBody() {
if (contentLength > 0)
_inputStream.skip(contentLength - _totalRead)
}
fun getHttpHeaderString(): String {
val writer = StringWriter();
writer.write(head + "\r\n");
@@ -139,14 +197,33 @@ class HttpContext : AutoCloseable {
}
fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) {
val bytes = body?.toByteArray(Charsets.UTF_8);
if(body != null && headers.get("content-length").isNullOrEmpty())
headers.put("content-length", bytes!!.size.toString());
if(headers.get("content-length").isNullOrEmpty()) {
if (body != null) {
headers.put("content-length", bytes!!.size.toString());
} else {
headers.put("content-length", "0")
}
}
respond(status, headers) { responseStream ->
if(body != null) {
responseStream.write(bytes!!);
}
}
}
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
if(headers.get("content-length").isNullOrEmpty()) {
if (body != null) {
headers.put("content-length", body.size.toString());
} else {
headers.put("content-length", "0")
}
}
respond(status, headers) { responseStream ->
if(body != null) {
responseStream.write(body);
}
}
}
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
@@ -161,8 +238,7 @@ class HttpContext : AutoCloseable {
headersToRespond.put("keep-alive", "timeout=5, max=1000");
}
val responseHeader = HttpResponse(status, headers);
val responseHeader = HttpResponse(status, headersToRespond);
responseStream.write(responseHeader.getHttpHeaderBytes());
if(method != "HEAD") {
@@ -172,38 +248,9 @@ class HttpContext : AutoCloseable {
statusCode = status;
}
fun readContentBytes(buffer: CharArray, length: Int) : Int {
val reading = Math.min(length, (contentLength - _totalRead).toInt());
val read = _stream.read(buffer, 0, reading);
_totalRead += read;
//TODO: Fix this properly
if(contentLength - _totalRead < 400 && read < length) {
_totalRead = contentLength;
}
return read;
}
fun readContentString() : String{
val writer = StringWriter();
var read = 0;
val buffer = CharArray(4096);
do {
read = readContentBytes(buffer, buffer.size);
writer.write(buffer, 0, read);
} while(read > 0);
return writer.toString();
}
inline fun <reified T> readContentJson() : T {
return Serializer.json.decodeFromString(readContentString());
}
fun skipBody() {
if(contentLength > 0)
_stream.skip(contentLength - _totalRead);
}
override fun close() {
if(!keepAlive) {
_stream?.close();
_inputStream.close();
_responseStream?.close();
}
}
@@ -1,12 +1,12 @@
package com.futo.platformplayer.api.http.server
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
import java.io.BufferedReader
import java.io.InputStreamReader
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
import com.futo.platformplayer.logging.Logger
import java.io.BufferedInputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.lang.reflect.Method
@@ -14,7 +14,7 @@ import java.net.InetAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.util.*
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.stream.IntStream.range
@@ -29,7 +29,8 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
var port = 0
private set;
private val _handlers = mutableListOf<HttpHandler>();
private val _handlers = hashMapOf<String, HashMap<String, HttpHandler>>()
private val _headHandlers = hashMapOf<String, HttpHandler>()
private var _workerPool: ExecutorService? = null;
@Synchronized
@@ -76,12 +77,12 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
private fun handleClientRequest(socket: Socket) {
_workerPool?.submit {
val requestReader = BufferedReader(InputStreamReader(socket.getInputStream()))
val requestStream = BufferedInputStream(socket.getInputStream());
val responseStream = socket.getOutputStream();
val requestId = UUID.randomUUID().toString().substring(0, 5);
try {
keepAliveLoop(requestReader, responseStream, requestId) { req ->
keepAliveLoop(requestStream, responseStream, requestId) { req ->
req.use { httpContext ->
if(!httpContext.path.startsWith("/plugin/"))
Logger.i(TAG, "[${req.id}] ${httpContext.method}: ${httpContext.path}")
@@ -107,7 +108,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
Logger.e(TAG, "Failed to handle client request.", e);
}
finally {
requestReader.close();
requestStream.close();
responseStream.close();
}
};
@@ -115,36 +116,82 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
fun getHandler(method: String, path: String) : HttpHandler? {
synchronized(_handlers) {
//TODO: Support regex paths?
if(method == "HEAD")
return _handlers.firstOrNull { it.path == path && (it.allowHEAD || it.method == "HEAD") }
return _handlers.firstOrNull { it.method == method && it.path == path };
if (method == "HEAD") {
return _headHandlers[path]
}
val handlerMap = _handlers[method] ?: return null
return handlerMap[path]
}
}
fun addHandler(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
synchronized(_handlers) {
_handlers.add(handler);
handler.allowHEAD = withHEAD;
var handlerMap: HashMap<String, HttpHandler>? = _handlers[handler.method];
if (handlerMap == null) {
handlerMap = hashMapOf()
_handlers[handler.method] = handlerMap
}
handlerMap[handler.path] = handler;
if (handler.allowHEAD || handler.method == "HEAD") {
_headHandlers[handler.path] = handler
}
}
return handler;
}
fun addHandlerWithAllowAllOptions(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
val allowedMethods = arrayListOf(handler.method, "OPTIONS")
if (withHEAD) {
allowedMethods.add("HEAD")
}
val tag = handler.tag
if (tag != null) {
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods).withTag(tag))
} else {
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods))
}
return addHandler(handler, withHEAD)
}
fun removeHandler(method: String, path: String) {
synchronized(_handlers) {
val handler = getHandler(method, path);
if(handler != null)
_handlers.remove(handler);
val handlerMap = _handlers[method] ?: return
val handler = handlerMap.remove(path) ?: return
if (method == "HEAD" || handler.allowHEAD) {
_headHandlers.remove(path)
}
}
}
fun removeAllHandlers(tag: String? = null) {
synchronized(_handlers) {
if(tag == null)
_handlers.clear();
else
_handlers.removeIf { it.tag == tag };
else {
for (pair in _handlers) {
val toRemove = ArrayList<String>()
for (innerPair in pair.value) {
if (innerPair.value.tag == tag) {
toRemove.add(innerPair.key)
if (pair.key == "HEAD" || innerPair.value.allowHEAD) {
_headHandlers.remove(innerPair.key)
}
}
}
for (x in toRemove)
pair.value.remove(x)
}
}
}
}
fun addBridgeHandlers(obj: Any, tag: String? = null) {
val tagToUse = tag ?: obj.javaClass.name;
//val tagToUse = tag ?: obj.javaClass.name;
val getMethods = obj::class.java.declaredMethods
.filter { it.getAnnotation(HttpGET::class.java) != null }
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
@@ -161,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for(getMethod in getMethods)
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
if(!getMethod.second.contentType.isEmpty())
this.withContentType(getMethod.second.contentType);
}.withContentType(getMethod.second.contentType ?: "");
}.withContentType(getMethod.second.contentType);
for(postMethod in postMethods)
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
if(!postMethod.second.contentType.isEmpty())
this.withContentType(postMethod.second.contentType);
}.withContentType(postMethod.second.contentType ?: "");
}.withContentType(postMethod.second.contentType);
for(getField in getFields) {
getField.first.isAccessible = true;
addHandler(HttpFuntionHandler("GET", getField.second.path) {
addHandler(HttpFunctionHandler("GET", getField.second.path) {
val value = getField.first.get(obj) as String?;
if(value != null) {
val headers = HttpHeaders(
@@ -184,13 +231,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
}
else
it.respondCode(204);
}).withContentType(getField.second.contentType ?: "");
}).withContentType(getField.second.contentType);
}
}
private fun keepAliveLoop(requestReader: BufferedReader, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
val stopCount = _stopCount;
var keepAlive = false;
var keepAlive: Boolean;
var requestsMax = 0;
var requestsTotal = 0;
do {
@@ -240,11 +287,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) {
if (!addr.isLoopbackAddress) {
val ipString: String = addr.hostAddress;
val isIPv4 = ipString.indexOf(':') < 0;
if (!isIPv4)
continue;
addresses.add(addr);
val ipString: String = addr.hostAddress ?: continue
val isIPv4 = ipString.indexOf(':') < 0
if (!isIPv4) {
continue
}
addresses.add(addr)
}
}
}
@@ -1,6 +1,3 @@
package com.futo.platformplayer.api.http.server.exceptions
import java.net.SocketTimeoutException
import java.util.concurrent.TimeoutException
class EmptyRequestException(msg: String) : Exception(msg) {}
@@ -7,7 +7,6 @@ class HttpConstantHandler(method: String, path: String, val content: String, val
val headers = this.headers.clone();
if(contentType != null)
headers["Content-Type"] = contentType;
headers["Content-Length"] = content.length.toString();
httpContext.respondCode(200, headers, content);
}
@@ -1,14 +1,16 @@
package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.logging.Logger
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.file.Files
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.GZIPOutputStream
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String, private val closeAfterRequest: Boolean = false): HttpHandler(method, path) {
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String): HttpHandler(method, path) {
override fun handle(httpContext: HttpContext) {
val requestHeaders = httpContext.headers;
val responseHeaders = this.headers.clone();
@@ -30,19 +32,13 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\""
val acceptEncoding = requestHeaders["Accept-Encoding"]
val shouldGzip = acceptEncoding != null && acceptEncoding.split(',').any { it.trim().equals("gzip", ignoreCase = true) || it == "*" }
if (shouldGzip) {
responseHeaders["Content-Encoding"] = "gzip"
}
val range = requestHeaders["Range"]
var start: Long
val start: Long
val end: Long
if (range != null && range.startsWith("bytes=")) {
val parts = range.substring(6).split("-")
start = parts[0].toLong()
end = parts.getOrNull(1)?.toLong() ?: (file.length() - 1)
end = parts.getOrNull(1)?.toLongOrNull() ?: (file.length() - 1)
responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}"
} else {
start = 0
@@ -51,18 +47,19 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
var totalBytesSent = 0
val contentLength = end - start + 1
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end, shouldGzip: $shouldGzip)")
responseHeaders["Content-Length"] = contentLength.toString()
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end)")
file.inputStream().use { inputStream ->
httpContext.respond(if (range == null) 200 else 206, responseHeaders) { responseStream ->
httpContext.respond(if (range != null) 206 else 200, responseHeaders) { responseStream ->
try {
val buffer = ByteArray(8192)
inputStream.skip(start)
var current = start
val outputStream = if (shouldGzip) GZIPOutputStream(responseStream) else responseStream
val outputStream = responseStream
while (true) {
val expectedBytesRead = (end - start + 1).coerceAtMost(buffer.size.toLong());
val expectedBytesRead = (end - current + 1).coerceAtMost(buffer.size.toLong());
val bytesRead = inputStream.read(buffer);
if (bytesRead < 0) {
Logger.i(TAG, "End of file reached")
@@ -73,27 +70,21 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
outputStream.write(buffer, 0, bytesToSend)
totalBytesSent += bytesToSend
Logger.v(TAG, "Sent bytes $start-${start + bytesToSend}, totalBytesSent=$totalBytesSent")
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
start += bytesToSend.toLong()
if (start >= end) {
current += bytesToSend.toLong()
if (current >= end) {
Logger.i(TAG, "Expected amount of bytes sent")
break
}
}
Logger.i(TAG, "Finished sending file (segment)")
if (shouldGzip) (outputStream as GZIPOutputStream).finish()
outputStream.flush()
} catch (e: Exception) {
httpContext.respondCode(500, headers)
}
}
if (closeAfterRequest) {
httpContext.keepAlive = false;
}
}
}
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
override fun handle(httpContext: HttpContext) {
httpContext.setResponseHeaders(this.headers);
handler(httpContext);
@@ -15,6 +15,7 @@ abstract class HttpHandler(val method: String, val path: String) {
headers.put(key, value);
return this;
}
fun withContentType(contentType: String) = withHeader("Content-Type", contentType);
fun withTag(tag: String) : HttpHandler {
@@ -2,19 +2,18 @@ package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext
class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) {
class HttpOptionsAllowHandler(path: String, val allowedMethods: List<String> = listOf()) : HttpHandler("OPTIONS", path) {
override fun handle(httpContext: HttpContext) {
//Just allow whatever is requested
val newHeaders = headers.clone()
newHeaders.put("Access-Control-Allow-Origin", "*")
val requestedOrigin = httpContext.headers.getOrDefault("Access-Control-Request-Origin", "");
val requestedMethods = httpContext.headers.getOrDefault("Access-Control-Request-Method", "");
val requestedHeaders = httpContext.headers.getOrDefault("Access-Control-Request-Headers", "");
val newHeaders = headers.clone();
newHeaders.put("Allow", requestedMethods);
newHeaders.put("Access-Control-Allow-Methods", requestedMethods);
newHeaders.put("Access-Control-Allow-Headers", "*");
if (allowedMethods.isNotEmpty()) {
newHeaders.put("Access-Control-Allow-Methods", allowedMethods.map { it.uppercase() }.joinToString(", "))
} else {
newHeaders.put("Access-Control-Allow-Methods", "*")
}
newHeaders.put("Access-Control-Allow-Headers", "*")
httpContext.respondCode(200, newHeaders);
}
}
@@ -1,11 +1,20 @@
package com.futo.platformplayer.api.http.server.handlers
import android.net.Uri
import android.util.Log
import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.parsers.HttpResponseParser
import com.futo.platformplayer.readLine
import java.io.InputStream
import java.io.OutputStream
import java.lang.Exception
import java.net.Socket
import javax.net.ssl.SSLSocketFactory
class HttpProxyHandler(method: String, path: String, val targetUrl: String): HttpHandler(method, path) {
class HttpProxyHandler(method: String, path: String, val targetUrl: String, private val useTcp: Boolean = false): HttpHandler(method, path) {
var content: String? = null;
var contentType: String? = null;
@@ -17,10 +26,17 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
private var _injectHost = false;
private var _injectReferer = false;
private val _client = ManagedHttpClient();
override fun handle(context: HttpContext) {
if (useTcp) {
handleWithTcp(context)
} else {
handleWithOkHttp(context)
}
}
private fun handleWithOkHttp(context: HttpContext) {
val proxyHeaders = HashMap<String, String>();
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
proxyHeaders[header.key] = header.value;
@@ -34,8 +50,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method;
//Logger.i(TAG, "Proxied Request ${useMethod}: ${targetUrl}");
//Logger.i(TAG, "Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}");
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
val resp = when (useMethod) {
"GET" -> _client.get(targetUrl, proxyHeaders);
@@ -44,8 +60,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
};
//Logger.i(TAG, "Proxied Response [${resp.code}]");
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) });
Logger.i(TAG, "Proxied Response [${resp.code}]");
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
for(newHeader in headers)
headersFiltered.put(newHeader.key, newHeader.value);
@@ -65,6 +81,140 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
}
}
private fun handleWithTcp(context: HttpContext) {
if (content != null)
throw NotImplementedError("Content body is not supported")
val proxyHeaders = HashMap<String, String>();
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
proxyHeaders[header.key] = header.value;
for (injectHeader in _injectRequestHeader)
proxyHeaders[injectHeader.first] = injectHeader.second;
val parsed = Uri.parse(targetUrl);
if(_injectHost)
proxyHeaders.put("Host", parsed.host!!);
if(_injectReferer)
proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method;
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
Logger.i(TAG, "handleWithTcp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
makeTcpRequest(proxyHeaders, useMethod, parsed, context)
}
private fun makeTcpRequest(proxyHeaders: HashMap<String, String>, useMethod: String, parsed: Uri, context: HttpContext) {
val requestBuilder = StringBuilder()
requestBuilder.append("$useMethod $parsed HTTP/1.1\r\n")
proxyHeaders.forEach { (key, value) -> requestBuilder.append("$key: $value\r\n") }
requestBuilder.append("\r\n")
val port = if (parsed.port == -1) {
when (parsed.scheme) {
"https" -> 443
"http" -> 80
else -> throw Exception("Unhandled scheme")
}
} else {
parsed.port
}
val socket = if (parsed.scheme == "https") {
val sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
sslSocketFactory.createSocket(parsed.host, port)
} else {
Socket(parsed.host, port)
}
socket.use { s ->
s.getOutputStream().write(requestBuilder.toString().encodeToByteArray())
val inputStream = s.getInputStream()
val resp = HttpResponseParser(inputStream)
if (resp.statusCode == 302) {
val location = resp.location!!
Logger.i(TAG, "handleWithTcp Proxied ${resp.statusCode} following redirect to $location");
makeTcpRequest(proxyHeaders, useMethod, Uri.parse(location)!!, context)
} else {
val isChunked = resp.transferEncoding.equals("chunked", ignoreCase = true)
val contentLength = resp.contentLength.toInt()
val headersFiltered = HttpHeaders(resp.headers.filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
for (newHeader in headers)
headersFiltered.put(newHeader.key, newHeader.value);
context.respond(resp.statusCode, headersFiltered) { responseStream ->
if (isChunked) {
Logger.i(TAG, "handleWithTcp handleChunkedTransfer");
handleChunkedTransfer(inputStream, responseStream)
} else if (contentLength > 0) {
Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength");
transferFixedLengthContent(inputStream, responseStream, contentLength)
} else if (contentLength == -1) {
Logger.i(TAG, "handleWithTcp transferUntilEndOfStream");
transferUntilEndOfStream(inputStream, responseStream)
} else {
Logger.i(TAG, "handleWithTcp no content");
}
}
}
}
}
private fun handleChunkedTransfer(inputStream: InputStream, responseStream: OutputStream) {
var line: String?
val buffer = ByteArray(8192)
while (inputStream.readLine().also { line = it } != null) {
val size = line!!.trim().toInt(16)
responseStream.write(line!!.encodeToByteArray())
responseStream.write("\r\n".encodeToByteArray())
if (size == 0) {
inputStream.skip(2)
responseStream.write("\r\n".encodeToByteArray())
break
}
var totalRead = 0
while (totalRead < size) {
val read = inputStream.read(buffer, 0, minOf(buffer.size, size - totalRead))
if (read == -1) break
responseStream.write(buffer, 0, read)
totalRead += read
}
inputStream.skip(2)
responseStream.write("\r\n".encodeToByteArray())
responseStream.flush()
}
}
private fun transferFixedLengthContent(inputStream: InputStream, responseStream: OutputStream, contentLength: Int) {
val buffer = ByteArray(8192)
var totalRead = 0
while (totalRead < contentLength) {
val read = inputStream.read(buffer, 0, minOf(buffer.size, contentLength - totalRead))
if (read == -1) break
responseStream.write(buffer, 0, read)
totalRead += read
}
responseStream.flush()
}
private fun transferUntilEndOfStream(inputStream: InputStream, responseStream: OutputStream) {
val buffer = ByteArray(8192)
var read: Int
while (inputStream.read(buffer).also { read = it } >= 0) {
responseStream.write(buffer, 0, read)
}
responseStream.flush()
}
fun withContent(body: String) : HttpProxyHandler {
this.content = body;
return this;
@@ -92,4 +242,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
_ignoreRequestHeaders.add("referer");
return this;
}
companion object {
private const val TAG = "HttpProxyHandler"
}
}
@@ -1,105 +0,0 @@
package com.futo.platformplayer.api.media
import androidx.collection.LruCache
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
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.live.ILiveChatWindowDescriptor
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.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
/**
* A temporary class that caches video results
* In future this should be part of a bigger system
*/
class CachedPlatformClient : IPlatformClient {
private val _client : IPlatformClient;
override val id: String get() = _client.id;
override val name: String get() = _client.name;
override val icon: ImageVariable? get() = _client.icon;
private val _cache: LruCache<String, IPlatformContentDetails>;
override val capabilities: PlatformClientCapabilities
get() = _client.capabilities;
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
this._client = client;
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
}
override fun initialize() { _client.initialize() }
override fun disable() { _client.disable() }
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
override fun getContentDetails(url: String): IPlatformContentDetails {
var result = _cache.get(url);
if(result == null) {
result = _client.getContentDetails(url);
if (result != null)
_cache.put(url, result);
}
return result;
}
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
override fun getChannelContents(
channelUrl: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
override fun search(
query: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
override fun searchChannelContents(
channelUrl: String,
query: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
override fun searchChannels(query: String) = _client.searchChannels(query);
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
override fun isClaimTypeSupported(claimType: Int): Boolean {
return _client.isClaimTypeSupported(claimType);
}
}
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media
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
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
@@ -13,7 +14,6 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
/**
* A client for a specific platform
@@ -85,6 +85,20 @@ interface IPlatformClient {
*/
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
/**
* Describes what the plugin is capable on peek channel results
*/
fun getPeekChannelTypes(): List<String>;
/**
* Peeks contents of a channel, upload time descending
*/
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
/**
* Gets all playlists of a channel
*/
fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist>
/**
* Gets the channel url associated with a claimType
*/
@@ -100,11 +114,18 @@ interface IPlatformClient {
*/
fun getContentDetails(url: String): IPlatformContentDetails;
fun getContentChapters(url: String): List<IChapter>;
/**
* Gets the playback tracker for a piece of content
*/
fun getPlaybackTracker(url: String): IPlaybackTracker?;
/**
* Get content recommendations
*/
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
//Comments
/**
@@ -9,7 +9,6 @@ import com.caverock.androidsvg.SVG
import com.futo.platformplayer.api.http.ManagedHttpClient
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.LiveEventDonation
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.structures.IPager
@@ -195,7 +194,7 @@ class LiveChatManager {
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
var drawable: Drawable? = null;
var url: String? = null;
var url: String?;
synchronized(_cache_lock) {
url = _cache_urls[emoji];
if(url != null)
@@ -13,9 +13,14 @@ data class PlatformClientCapabilities(
val hasGetChannelUrlByClaim: Boolean = false,
val hasGetChannelTemplateByClaimMap: Boolean = false,
val hasGetSearchCapabilities: Boolean = false,
val hasGetSearchChannelContentsCapabilities: Boolean = false,
val hasGetChannelCapabilities: Boolean = false,
val hasGetLiveEvents: Boolean = false,
val hasGetLiveChatWindow: Boolean = false
val hasGetLiveChatWindow: Boolean = false,
val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false
) {
}
@@ -13,13 +13,15 @@ class PlatformClientPool {
private val _pool: HashMap<JSClient, Int> = hashMapOf();
private var _poolCounter = 0;
private val _poolName: String?;
private val _privatePool: Boolean;
var isDead: Boolean = false
private set;
val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null) {
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
_poolName = name;
_privatePool = privatePool;
if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started");
@@ -51,7 +53,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();
reserved = _parent.getCopy(_privatePool);
reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);
@@ -6,12 +6,14 @@ class PlatformMultiClientPool {
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
private var _isFake = false;
private var _privatePool = false;
constructor(name: String, maxCap: Int = -1) {
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
_name = name;
_maxCap = if(maxCap > 0)
maxCap
else 99;
_privatePool = isPrivatePool;
}
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@@ -19,8 +21,8 @@ class PlatformMultiClientPool {
return parentClient;
val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
this.onDead.subscribe { client, pool ->
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
this.onDead.subscribe { _, pool ->
synchronized(_clientPools) {
if(_clientPools[parentClient] == pool)
_clientPools.remove(parentClient);
@@ -10,11 +10,11 @@ import com.futo.platformplayer.getOrThrow
* A link to a channel, often with its own name and thumbnail
*/
@kotlinx.serialization.Serializable
class PlatformAuthorLink {
open class PlatformAuthorLink {
val id: PlatformID;
val name: String;
val url: String;
val thumbnail: String?;
var thumbnail: String?;
var subscribers: Long? = null; //Optional
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
@@ -28,6 +28,9 @@ class PlatformAuthorLink {
companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value);
val context = "AuthorLink"
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context),
@@ -0,0 +1,33 @@
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.getOrDefault
import com.futo.platformplayer.getOrThrow
/**
* A link to a channel, often with its own name and thumbnail
*/
@kotlinx.serialization.Serializable
class PlatformAuthorMembershipLink: PlatformAuthorLink {
val membershipUrl: String?;
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null, membershipUrl: String? = null): super(id, name, url, thumbnail, subscribers)
{
this.membershipUrl = membershipUrl;
}
companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context),
value.getOrThrow(config, "url", context),
value.getOrDefault<String>(config, "thumbnail", context, null),
if(value.has("subscribers")) value.getOrThrow(config,"subscribers", context) else null,
if(value.has("membershipUrl")) value.getOrThrow(config, "membershipUrl", context) else null
);
}
}
}
@@ -29,6 +29,7 @@ class ResultCapabilities(
const val TYPE_LIVE = "LIVE";
const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED";
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
@@ -63,7 +64,6 @@ class FilterGroup(
val isMultiSelect: Boolean,
val id: String? = null
) {
@kotlinx.serialization.Transient
val idOrName: String get() = id ?: name;
companion object {
@@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
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.getOrDefault
import com.futo.platformplayer.getOrThrow
@kotlinx.serialization.Serializable
@@ -20,6 +22,10 @@ class Thumbnails {
fun getLQThumbnail() : String? {
return sources.firstOrNull()?.url;
}
fun getMinimumThumbnail(quality: Int): String? {
return sources.firstOrNull { it.quality >= quality }?.url ?: getHQThumbnail();
}
fun hasMultiple() = sources.size > 1;
@@ -27,7 +33,7 @@ class Thumbnails {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray()
.map { Thumbnail.fromV8(it as V8ValueObject) }
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
.toTypedArray());
}
}
@@ -36,10 +42,10 @@ class Thumbnails {
data class Thumbnail(val url : String?, val quality : Int = 0) {
companion object {
fun fromV8(value: V8ValueObject): Thumbnail {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
return Thumbnail(
value.getString("url"),
value.getInteger("quality"));
value.getOrDefault<String>(config,"url", "Thumbnail", null),
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
}
}
};
@@ -37,6 +37,10 @@ class SerializedChannel(
TODO("Not yet implemented")
}
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel(
@@ -0,0 +1,32 @@
package com.futo.platformplayer.api.media.models.chapters
import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
import com.futo.platformplayer.api.media.models.contents.ContentType
interface IChapter {
val name: String;
val type: ChapterType;
val timeStart: Double;
val timeEnd: Double;
}
enum class ChapterType(val value: Int) {
NORMAL(0),
SKIPPABLE(5),
SKIP(6),
SKIPONCE(7);
companion object {
fun fromInt(value: Int): ChapterType
{
val result = ChapterType.entries.firstOrNull { it.value == value };
if(result == null)
throw UnknownPlatformException(value.toString());
return result;
}
}
}
@@ -4,10 +4,7 @@ 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.structures.IPager
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.Pointer
import com.futo.polycentric.core.SignedEvent
import userpackage.Protocol.Reference
import java.time.OffsetDateTime
@@ -20,16 +17,20 @@ class PolycentricPlatformComment : IPlatformComment {
override val replyCount: Int?;
val eventPointer: Pointer;
val reference: Reference;
val parentReference: Reference?;
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, reference: Reference, replyCount: Int? = null) {
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
this.contextUrl = contextUrl;
this.author = author;
this.message = msg;
this.rating = rating;
this.date = date;
this.replyCount = replyCount;
this.reference = reference;
this.eventPointer = eventPointer;
this.reference = eventPointer.toReference();
this.parentReference = parentReference;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@@ -37,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
}
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
}
companion object {
private const val TAG = "PolycentricPlatformComment"
val MAX_COMMENT_SIZE = 2000
}
}
@@ -13,6 +13,7 @@ enum class ContentType(val value: Int) {
NESTED_VIDEO(11),
LOCKED(70),
PLACEHOLDER(90),
DEFERRED(91);
@@ -20,7 +21,7 @@ enum class ContentType(val value: Int) {
companion object {
fun fromInt(value: Int): ContentType
{
val result = ContentType.values().firstOrNull { it.value == value };
val result = ContentType.entries.firstOrNull { it.value == value };
if(result == null)
throw UnknownPlatformException(value.toString());
return result;
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
fun getPlaybackTracker(): IPlaybackTracker?;
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
}
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.live
interface ILiveChatWindowDescriptor {
val url: String;
val removeElements: List<String>;
val removeElementsInterval: List<String>;
}
@@ -1,31 +1,23 @@
package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.ratings.RatingScaler
import com.futo.platformplayer.api.media.models.ratings.RatingType
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault
interface IPlatformLiveEvent {
val type : LiveEventType;
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IPlatformLiveEvent {
val contextName = "LiveEvent";
val type = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(type) {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
else -> throw NotImplementedError("Unknown type ${type}");
else -> throw NotImplementedError("Unknown type $t");
}
}
}
@@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
companion object{
fun fromInt(value : Int) : LiveEventType{
return LiveEventType.values().first { it.value == value };
return LiveEventType.entries.first { it.value == value };
}
}
}
@@ -0,0 +1,13 @@
package com.futo.platformplayer.api.media.models.locked
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
interface IPlatformLockedContent: IPlatformContent {
val lockContentType: ContentType;
val lockDescription: String?;
val unlockUrl: String?;
val contentName: String?;
val contentThumbnails: Thumbnails;
}
@@ -0,0 +1,14 @@
package com.futo.platformplayer.api.media.models.modifier
class AdhocRequestModifier: IRequestModifier {
val _handler: (String, Map<String,String>)->IRequest;
override var allowByteSkip: Boolean = false;
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
_handler = modifyReq;
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _handler(url, headers);
}
}
@@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.models.modifier
interface IModifierOptions {
val applyAuthClient: String?;
val applyCookieClient: String?;
val applyOtherHeaders: Boolean;
}
@@ -0,0 +1,6 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val url: String?;
val headers: Map<String, String>;
}
@@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequestModifier {
var allowByteSkip: Boolean;
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
}
@@ -7,4 +7,5 @@ interface IPlaybackTracker {
fun onInit(seconds: Double);
fun onProgress(seconds: Double, isPlaying: Boolean);
fun onConcluded();
}
@@ -1,9 +1,6 @@
package com.futo.platformplayer.api.media.models.playlists
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
interface IPlatformPlaylist : IPlatformContent {
val thumbnail: String?;
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
//TODO: Determine if this should be IPlatformContent (probably not?)
val contents: IPager<IPlatformVideo>;
fun toPlaylist(): Playlist;
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
}
@@ -2,10 +2,6 @@ package com.futo.platformplayer.api.media.models.post
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
/**
* A detailed video model with data including video/audio sources
@@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
companion object {
fun fromInt(value: Int): TextType
{
val result = TextType.values().firstOrNull { it.value == value };
val result = TextType.entries.firstOrNull { it.value == value };
if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value");
return result;
@@ -14,14 +14,13 @@ interface IRating {
companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IRating {
val contextName = "Rating";
val type = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(type) {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
RatingType.SCALE -> RatingScaler.fromV8(config, obj);
else -> throw NotImplementedError("Unknown type ${type}");
else -> throw NotImplementedError("Unknown type $t");
}
}
}
@@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
companion object{
fun fromInt(value : Int) : RatingType{
return RatingType.values().first { it.value == value };
return RatingType.entries.first { it.value == value };
}
}
}
@@ -0,0 +1,51 @@
package com.futo.platformplayer.api.media.models.streams.sources
import android.net.Uri
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
class HLSVariantVideoUrlSource(
override val name: String,
override val width: Int,
override val height: Int,
override val container: String,
override val codec: String,
override val bitrate: Int?,
override val duration: Long,
override val priority: Boolean,
val url: String
) : IVideoUrlSource {
override fun getVideoUrl(): String {
return url
}
}
class HLSVariantAudioUrlSource(
override val name: String,
override val bitrate: Int,
override val container: String,
override val codec: String,
override val language: String,
override val duration: Long?,
override val priority: Boolean,
val url: String
) : IAudioUrlSource {
override fun getAudioUrl(): String {
return url
}
}
class HLSVariantSubtitleUrlSource(
override val name: String,
override val url: String,
override val format: String,
) : ISubtitleSource {
override val hasFetch: Boolean = false
override fun getSubtitles(): String? {
return null
}
override suspend fun getSubtitlesURI(): Uri? {
return Uri.parse(url)
}
}
@@ -0,0 +1,6 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource {
val bearerToken: String
val licenseUri: String
}
@@ -1,8 +1,6 @@
package com.futo.platformplayer.api.media.models.subtitles
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
interface ISubtitleSource {
val name: String;
@@ -1,13 +1,12 @@
package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.*
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.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.structures.IPager
/**
* A detailed video model with data including video/audio sources

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