Compare commits

..

535 commits
231 ... master

Author SHA1 Message Date
Koen J
daa91986ef Add search type selector to suggestions fragment. 2025-04-22 13:40:05 +02:00
Koen J
63761cfc9a Simplified all searches to use ContentSearchResultsFragment. 2025-04-22 13:08:23 +02:00
Koen J
d10026acd1 Added ping loop. 2025-04-21 13:32:58 +02:00
Koen J
9347351c37 Fixed issue where it would continuously try to connect over relay. 2025-04-15 09:39:35 +02:00
Koen J
0ef1f2d40f Added LinkType to Channel. 2025-04-14 15:19:16 +02:00
Koen J
b460f9915d Added settings for enabling/disabling remote sync features. Fixed device pairing success showing too early. 2025-04-14 14:41:47 +02:00
Koen J
4e195dfbc3 Rename to direct and relayed. 2025-04-14 10:38:42 +02:00
Koen
3c7f7bfca7 Merge branch 'remote-sync' into 'master'
Implemented remote sync.

See merge request videostreaming/grayjay!93
2025-04-11 14:31:47 +00:00
Koen
05230971b3 Implemented remote sync. 2025-04-11 14:31:47 +00:00
Kelvin K
dccdf72c73 Message change 2025-04-09 23:35:44 +02:00
Kelvin K
ca15983a72 Casting message, caching creator images 2025-04-09 23:26:35 +02:00
Kelvin K
4b6a2c9829 Keyboard hide on search end 2025-04-09 21:02:19 +02:00
Kelvin K
1755d03a6b Fcast clearer connection/reconnection overlay, disable ipv6 by default 2025-04-09 00:56:49 +02:00
Kelvin K
869b1fc15e Fix pager for landscape 2025-04-08 00:34:52 +02:00
Kelvin K
ce2a2f8582 submods 2025-04-07 23:32:57 +02:00
Kelvin K
7b355139fb Subscription persistence fixes, home toggle fixes, subs exchange gzip, etc 2025-04-07 23:31:00 +02:00
Kelvin K
b14518edb1 Home filter fixes, persistent sorts, subs exchange fixes, playlist video options 2025-04-05 01:02:50 +02:00
Kelvin K
7d64003d1c Feed filter loading improved, home filters support, various peripheral stuff 2025-04-04 00:37:26 +02:00
Kelvin K
0a59e04f19 Fix ui offset issue when opening video through search url 2025-04-02 23:40:37 +02:00
Kelvin
b57abb646f Merge branch 'subs-exchange' into 'master'
Experimental Subs Exchange

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

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

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

Changelog: changed
2025-02-10 22:32:56 -06:00
Kelvin
e36047c890 Merge branch 'prevent-exception-replay' into 'master'
Prevent Exception Replay When Unsubscribing From Deleted Channel

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

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

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

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

See merge request videostreaming/grayjay!76
2025-02-10 18:47:50 +00:00
Kelvin
72f5b5fbc0 Ref 2025-02-07 19:08:38 +01:00
Kelvin
330aa495c8 Playlist dup prevention, download search and ordering, optional package support 2025-02-06 21:36:33 +01:00
Kelvin
0b529ae94d Plugin changelog support, Hide hidden from search setting, No author change warning if missing pubkey, toast on add to playlist, better autoplay description, Playlist total duration label 2025-02-06 19:19:29 +01:00
Kelvin
83b35183d0 Channel shorts tab, Forced batch parallelization, Playlist download support for live sources, hardware codec query 2025-02-05 19:40:28 +01:00
Kelvin
2cd01eb1fe Merge 2025-02-03 21:38:18 +01:00
Kelvin
07378f665a Fix http memory leak for byte responses, refs 2025-02-03 21:36:41 +01:00
Kai DeLorenzo
bfd5f24f4c Fix https://github.com/futo-org/grayjay-android/issues/727 2025-01-27 21:51:17 +00:00
Kai
3d617187af
update rotation lock approach
Changelog: changed
2025-01-27 12:03:25 -06:00
Koen J
d040b93ca9 Updated submodules and fixed IPv6 casting play address being wrong. 2025-01-23 14:26:08 +01:00
Koen J
a410e2962a Only take one line for signing. 2025-01-20 14:04:55 +01:00
Koen J
f5aa8f37bb Updated youtube. 2025-01-17 22:19:02 +01:00
Koen J
7e932df450 Updated submodules. 2025-01-17 22:01:26 +01:00
Koen J
3d4741727e Updated submodules. 2025-01-17 21:37:28 +01:00
Kelvin
a03b63ef74 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-01-17 21:27:23 +01:00
Kelvin
15ce3e9f20 Timeout support 2025-01-17 21:27:12 +01:00
Kai
da58b72f9d
add background playback support for videos without an explicit audio source
Changelog: changed
2025-01-14 11:36:37 -06:00
Kai DeLorenzo
1639bd7af1 Merge branch 'improve-full-screen-portrait-docs' into 'master'
improve full screen portrait docs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

See merge request videostreaming/grayjay!35
2024-09-26 12:40:47 +00:00
Koen
1fe9b70176 Sync protocol 2024-09-26 12:40:47 +00:00
Kai
a9cf8dd71a
more sizing fixes 2024-09-12 10:34:46 -05:00
Kai
3299261db3
fixed video sizing 2024-09-12 10:28:06 -05:00
Kai
e465ec8278
remove auto fullscreen for tablets 2024-09-12 08:47:00 -05:00
Kai
d0e4a0aa1f
update settings 2024-09-11 21:45:18 -05:00
Kai
74efec3235
Merge branch 'refs/heads/master' into landscape
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/SimpleOrientationListener.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
2024-09-11 21:37:34 -05:00
Kai
13516087f2
minimized player fixes 2024-09-11 20:24:16 -05:00
Kelvin
0a0c16524a Allow hiding privacy mode and FAQ without breaking existing orderings 2024-09-10 23:10:36 +02:00
Kelvin
9b843a155e Revert "Allow more tabs to be hidden."
This reverts commit 8c4e511883
2024-09-10 20:51:30 +00:00
Kelvin
cb085acbff Submods 2024-09-10 21:06:30 +02:00
Kelvin
c3d7df166b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 20:40:17 +02:00
Kelvin
d312062125 Fix content recommendations on offline videos 2024-09-10 20:40:14 +02:00
Koen J
e2453192aa More gracefully handle failing to set plugin auth. 2024-09-10 17:27:00 +02:00
Kai
68eb0cc8f2
added comment 2024-09-10 09:40:24 -05:00
Kai
cb9cecfa5d
rotation fixes 2024-09-10 09:36:59 -05:00
Koen J
0f4e4a7d97 Allow configuring stability threshold time and ensure there is no more than 1 job active at a time for SimpleOrientationListener. 2024-09-10 15:54:27 +02:00
Koen J
f20a708b36 Check both length and null for 'No recommendations found' 2024-09-10 12:25:30 +02:00
Koen J
8c4e511883 Allow more tabs to be hidden. 2024-09-10 12:24:42 +02:00
Koen J
a4a3b8d664 Implement full autorotate lock (default off). 2024-09-10 11:59:44 +02:00
Koen J
bf6530ea81 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 10:34:46 +02:00
Koen J
4a80c2aab1 Moved Autoplay button to top and load recommendations now appropriately uses 'StatePlatform.instance.getContentRecommendations(v.url)' for local videos. 2024-09-10 10:34:36 +02:00
Kelvin
527bbfe43f Fix watchlater re-downloading every time videos are reordered 2024-09-09 23:07:15 +02:00
Koen J
d8e1edb60b Added autoplay icon. 2024-09-09 15:52:55 +02:00
Koen J
245b5f74c0 Increased scrubber size a bit and made add comment view invisible for platform comments. 2024-09-09 15:50:36 +02:00
Koen J
e9a1f63415 Added autoplay setting. 2024-09-09 15:20:31 +02:00
Koen J
ec370dd94b Added autoplay feature. 2024-09-09 14:58:08 +02:00
Koen J
e39d862ef3 Added rotation zone setting allowing you to specify the rotation to be less sensitive (default 45 degrees). Added reverse portrait setting allowing you to allow reverse portrait (default off). Added setting to hide recommendations. 2024-09-09 12:41:16 +02:00
Koen J
7b065654aa Updated submodules. 2024-09-09 10:52:52 +02:00
Kelvin
918b2bbe96 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 18:46:33 +02:00
Kelvin
e529a3d34d Temporariyl disable video cache 2024-09-06 18:46:26 +02:00
Koen J
5475778d67 Force reload. 2024-09-06 18:24:52 +02:00
Kelvin
c6a3ff0a53 Stable ref 2024-09-06 17:42:28 +02:00
Kelvin
cf3587f504 last selected comment section option and default 2024-09-06 17:21:50 +02:00
Kelvin
d42f104884 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 16:29:31 +02:00
Kelvin
6a43568369 Dialog improvement, prep dialog 2024-09-06 16:29:24 +02:00
Koen J
85c9cd0a6e Recommendation mostly finished. 2024-09-06 16:00:13 +02:00
Koen J
be5920cfae Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 15:36:05 +02:00
Koen J
3d25d94a77 Mostly implemented recommendations. 2024-09-06 15:35:59 +02:00
Kelvin
fe97850835 Remove target size 2024-09-06 15:33:55 +02:00
Kelvin
dab9decd89 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 15:32:58 +02:00
Kelvin
854651aa71 Fix notification thumbnail pixelation on newer androids 2024-09-06 15:32:49 +02:00
Koen J
fdd1af3287 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 10:13:09 +02:00
Koen J
0bf92b6aff Home button refresh added. Possible fix for Grayjay starting playback after hours of being inactive. Scroll to top and reload feed. 2024-09-06 10:12:23 +02:00
Kelvin
d9403bf4da Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-05 20:18:37 +02:00
Kelvin
716d8caf4d SLD crash fix 2024-09-05 20:18:29 +02:00
Koen J
0f0f368a75 Do not allow downloading/editing name of temporary playlist. 2024-09-05 13:14:41 +02:00
Koen J
ff8d7558d4 Re-added bypass rotation prevention. 2024-09-05 12:42:12 +02:00
Koen J
66f9824b68 Finetuning rotation. 2024-09-05 10:53:42 +02:00
Koen J
44a6e5da38 Added background subscription upadte failed toast and removed home page refresh when older than a minute. 2024-09-05 10:04:53 +02:00
Kelvin
de5a4aa5f3 Duplicate client id dialog and filtering, scrollable code block for dialogs 2024-09-04 21:24:26 +02:00
Kelvin
e8007082a7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-04 20:02:55 +02:00
Kelvin
3c70c5a366 Better handling of null author, search url fixes, Handling of more than 1000 subscriptions 2024-09-04 20:02:44 +02:00
Koen J
eb6e79b055 Catch WorkManager crash. 2024-09-04 14:24:11 +02:00
Kelvin
ea59f8dccb Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-03 22:33:04 +02:00
Kelvin
aef1c584e5 Article spec structures 2024-09-03 22:32:50 +02:00
Koen J
c4ce671a87 Fixed crash on Android 10 related to showing and hiding system UI when entering fullscreen. Made Platform comments the default. 2024-09-03 19:59:27 +02:00
Koen J
e8a79c87ab Added additional palce where isTransientLoss is reset. 2024-09-03 13:03:18 +02:00
Koen J
249e77a5d3 More audio focus changes. 2024-09-03 12:59:06 +02:00
Koen J
3cf4a52a69 Removed multicast lock again. 2024-09-03 11:48:35 +02:00
Koen J
eb8b02756b Fixed case where device fails to acquire audio focus. 2024-09-03 11:13:23 +02:00
Koen J
0510d34ed3 Updated Bilibili 2024-09-02 20:10:02 +02:00
Koen J
1c8d12e72a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-02 19:45:35 +02:00
Koen J
0a36a6b674 Increased byte array size mDNS. 2024-09-02 19:45:27 +02:00
Kelvin
b887c9d50f Change settings, WIP article object' 2024-09-02 19:32:59 +02:00
Koen J
ee4e108e4f Acquiring and releasing multicast lock. 2024-09-02 17:57:51 +02:00
Koen J
5e14a0fed4 Possible fix for receivers. 2024-09-02 17:45:11 +02:00
Koen J
6045205ea9 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-02 16:17:09 +02:00
Koen J
f2d763cdec Updated Rumble and Youtube and added tagName and parentElement to DOMParser. 2024-09-02 16:17:01 +02:00
Kelvin
e5e348205a atob/btoa as methods, string body fix for devportal 2024-09-02 15:21:09 +02:00
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
Kai DeLorenzo
da6eef905c
hide unable to resolve host exceptions when there are more videos in a playlist 2024-07-05 11:48:35 -05:00
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
566 changed files with 35936 additions and 5487 deletions

94
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,94 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug"]
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"
- "Odysee"
- "Rumble"
- "Kick"
- "Twitch"
- "PeerTube"
- "Patreon"
- "Nebula"
- "BiliBili (CN)"
- "Bitchute"
- "SoundCloud"
- "Dailymotion"
- "Apple Podcasts"
- "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: dropdown
id: vpn
attributes:
label: Are you using a VPN?
multiple: false
options:
- "No"
- "Yes"
validations:
required: true
- 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
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
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

View file

@ -0,0 +1,63 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["Documentation"]
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.

View file

@ -0,0 +1,56 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
labels: ["Enhancement"]
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
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.

36
.gitmodules vendored
View file

@ -58,3 +58,39 @@
[submodule "dep/futopay"] [submodule "dep/futopay"]
path = dep/futopay path = dep/futopay
url = ../futopayclientlibraries.git 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
[submodule "app/src/stable/assets/sources/apple-podcast"]
path = app/src/stable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
path = app/src/unstable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/stable/assets/sources/tedtalks"]
path = app/src/stable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/unstable/assets/sources/tedtalks"]
path = app/src/unstable/assets/sources/tedtalks
url = ../plugins/tedtalks.git

View file

@ -49,9 +49,23 @@ We encourage developers to write their own plugins. Please refer to the "Getting
## Contributing to Core ## Contributing to Core
**We are currently not accepting contributions to the core.**
The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity. ### License
The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/).
### How to Contribute
1. Fork the core repository.
2. Clone your fork.
3. Make your changes.
4. Commit and push your changes.
5. Open a pull request.
### Guidelines
- Ensure your code adheres to the existing style.
- Include documentation and unit tests (where applicable).
--- ---

32
LICENSE
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
LICENSE.md Normal file
View file

@ -0,0 +1,43 @@
# Source First License 1.1
## 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 license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company.
## Notices
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.
## 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.

View file

@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Video</td> <td>Video</td>
@ -24,12 +24,10 @@ The FUTO media app is a player that exposes multiple video websites as sources i
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Sources (all enabled)</td> <td>Sources</td>
<td>Sources (one disabled)</td>
</tr> </tr>
</table> </table>
@ -38,7 +36,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Install a new source</td> <td>Install a new source</td>
@ -54,8 +52,8 @@ When a user enters a search term into the search bar, the query is posted to th
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Search (list)</td> <td>Search (list)</td>
@ -71,7 +69,7 @@ Creators are able to configure their profile using NeoPass.
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Channel</td> <td>Channel</td>
@ -112,7 +110,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Settings</td> <td>Settings</td>
@ -125,8 +123,8 @@ Playlists allow you to make a collection of videos that you can create and custo
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Playlists</td> <td>Playlists</td>
@ -142,7 +140,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Downloads</td> <td>Downloads</td>
@ -157,7 +155,7 @@ For more information about casting please click [here](./docs/casting.md).
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Casting</td> <td>Casting</td>
@ -182,6 +180,12 @@ In the future we hope to offer users the choice of their desired recommendation
1. Download a copy of the repository. 1. Download a copy of the repository.
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository. 2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
3. Open the terminal in Android Studio by clicking on the terminal icon on bottom left and run the following command:
```sh
git submodule update --init --recursive
```
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator. 3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes. 4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
@ -199,7 +203,6 @@ Create a tag on the master branch, incrementing the last version number by 1 (fo
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update. Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
## Documentation ## Documentation
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview). The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).

View file

@ -2,7 +2,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21' id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
id 'org.ajoberstar.grgit' version '1.7.2' id 'org.ajoberstar.grgit' version '5.2.2'
id 'com.google.protobuf' id 'com.google.protobuf'
id 'kotlin-parcelize' id 'kotlin-parcelize'
id 'com.google.devtools.ksp' id 'com.google.devtools.ksp'
@ -144,9 +144,19 @@ android {
buildFeatures { buildFeatures {
buildConfig true buildConfig true
} }
sourceSets {
main {
assets {
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
}
}
}
} }
dependencies { dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core //Core
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
@ -184,11 +194,10 @@ dependencies {
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
//Other //Other
implementation 'org.jmdns:jmdns:3.5.1'
implementation 'org.jsoup:jsoup:1.15.3' implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:5.1' implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:core:3.4.1'

View file

@ -0,0 +1,266 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import java.net.Socket
import java.nio.ByteBuffer
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
class SyncServerTests {
//private val relayHost = "relay.grayjay.app"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.175"
private val relayPort = 9000
/** Creates a client connected to the live relay server. */
private suspend fun createClient(
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
isHandshakeAllowed: ((SyncSocketSession, String, String?) -> Boolean)? = null
): SyncSocketSession = withContext(Dispatchers.IO) {
val p = Noise.createDH("25519")
p.generateKeyPair()
val socket = Socket(relayHost, relayPort)
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
val tcs = CompletableDeferred<Boolean>()
val socketSession = SyncSocketSession(
relayHost,
p,
inputStream,
outputStream,
onClose = { socket.close() },
onHandshakeComplete = { s ->
onHandshakeComplete?.invoke(s)
tcs.complete(true)
},
onData = onData ?: { _, _, _, _ -> },
onNewChannel = onNewChannel ?: { _, _ -> },
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _ -> true }
)
socketSession.authorizable = AlwaysAuthorized()
socketSession.startAsInitiator(relayKey)
withTimeout(5000.milliseconds) { tcs.await() }
return@withContext socketSession
}
@Test
fun multipleClientsHandshake_Success() = runBlocking {
val client1 = createClient()
val client2 = createClient()
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
client1.stop()
client2.stop()
}
@Test
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
delay(100.milliseconds)
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
assertNotNull("Client B should receive connection info", infoB)
assertEquals(12345.toUShort(), infoB!!.port)
assertNull("Client C should not receive connection info (unauthorized)", infoC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun relayedTransport_Bidirectional_Success() = runBlocking {
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
val tcsDataA = CompletableDeferred<ByteArray>()
channelA.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
}
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
assertArrayEquals(maxSizeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
val data = byteArrayOf(1, 2, 3)
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
assertTrue(success)
assertNotNull(recordB)
assertArrayEquals(data, recordB!!.first)
assertNull("Unauthorized client should not access record", recordC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun getNonExistentRecord_ReturnsNull() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
assertNull("Getting non-existent record should return null", record)
clientA.stop()
clientB.stop()
}
@Test
fun updateRecord_TimestampUpdated() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val key = "updateKey"
val data1 = byteArrayOf(1)
val data2 = byteArrayOf(2)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
val record1 = clientB.getRecord(clientA.localPublicKey, key)
delay(1000.milliseconds)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
val record2 = clientB.getRecord(clientA.localPublicKey, key)
assertNotNull(record1)
assertNotNull(record2)
assertTrue(record2!!.second > record1!!.second)
assertArrayEquals(data2, record2.first)
clientA.stop()
clientB.stop()
}
@Test
fun deleteRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val data = byteArrayOf(1, 2, 3)
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
assertTrue(success)
assertNull(record)
clientA.stop()
clientB.stop()
}
@Test
fun listRecordKeys_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val keys = arrayOf("key1", "key2", "key3")
keys.forEach { key ->
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
}
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
clientA.stop()
clientB.stop()
}
@Test
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
assertArrayEquals(largeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetLargeRecord_Success() = runBlocking {
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
val clientA = createClient()
val clientB = createClient()
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
assertTrue(success)
assertNotNull(record)
assertArrayEquals(largeData, record!!.first)
clientA.stop()
clientB.stop()
}
}
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
}

View file

@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <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="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_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_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/> <uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
@ -35,15 +36,18 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider> </provider>
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service android:name=".services.MediaPlaybackService" <service android:name=".services.MediaPlaybackService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaPlayback" /> android:foregroundServiceType="mediaPlayback" />
<service android:name=".services.DownloadService" <service android:name=".services.DownloadService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name=".services.ExportingService"
android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" /> <receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" /> <receiver android:name=".receivers.AudioNoisyReceiver" />
@ -53,9 +57,8 @@
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true" android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleInstance"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true">
@ -148,34 +151,30 @@
<data android:scheme="polycentric" /> <data android:scheme="polycentric" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".activities.TestActivity" android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SettingsActivity" android:name=".activities.SettingsActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.DeveloperActivity" android:name=".activities.DeveloperActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.ExceptionActivity" android:name=".activities.ExceptionActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.CaptchaActivity" android:name=".activities.CaptchaActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.LoginActivity" android:name=".activities.LoginActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.AddSourceActivity" android:name=".activities.AddSourceActivity"
android:screenOrientation="portrait"
android:exported="true" android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"> android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter> <intent-filter>
@ -189,44 +188,55 @@
</activity> </activity>
<activity <activity
android:name=".activities.AddSourceOptionsActivity" android:name=".activities.AddSourceOptionsActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricHomeActivity" android:name=".activities.PolycentricHomeActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricBackupActivity" android:name=".activities.PolycentricBackupActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricCreateProfileActivity" android:name=".activities.PolycentricCreateProfileActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricProfileActivity" android:name=".activities.PolycentricProfileActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricWhyActivity" android:name=".activities.PolycentricWhyActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricImportProfileActivity" android:name=".activities.PolycentricImportProfileActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.ManageTabsActivity" android:name=".activities.ManageTabsActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.QRCaptureActivity" android:name=".activities.QRCaptureActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.FCastGuideActivity" android:name=".activities.FCastGuideActivity"
android:screenOrientation="portrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncHomeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncPairActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application> </application>
</manifest> </manifest>

View file

@ -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

View file

@ -262,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
.then(x=>x.json()) .then(x=>x.json())
.then(y=> cb && cb(y)); .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) { function sendFakeDevLog(devId, msg) {
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {}); return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
} }

View file

@ -7,6 +7,9 @@
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">--> <!--<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"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<style> <style>
@ -150,7 +153,7 @@
.pastPluginUrl { .pastPluginUrl {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 500px; width: 700px;
text-align: center; text-align: center;
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
@ -160,13 +163,122 @@
box-shadow: 0px 1px 2px #131313; box-shadow: 0px 1px 2px #131313;
font-weight: lighter; font-weight: lighter;
cursor: pointer; 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> </style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<v-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 id="topMenu">
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;"> <div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
<img src="./dependencies/FutoMainLogo.svg" <img src="./dependencies/FutoMainLogo.svg"
@ -250,10 +362,13 @@
</div> </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> <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)"> <div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
{{pastPluginUrl}} {{pastPluginUrl}}
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
X
</div>
</div> </div>
</div> </div>
</div> </div>
@ -402,6 +517,11 @@
<div class="code"> <div class="code">
{{req.code}} {{req.code}}
</div> </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>
<div class="parameter" v-for="parameter in req.parameters"> <div class="parameter" v-for="parameter in req.parameters">
<div class="name"> <div class="name">
@ -500,7 +620,62 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <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-actions>
</v-card> </v-card>
</div> </div>
@ -538,6 +713,7 @@
<!--<script src="./dependencies/vue.js"></script>--> <!--<script src="./dependencies/vue.js"></script>-->
<!--<script src="./dependencies/vuetify.js"></script>--> <!--<script src="./dependencies/vuetify.js"></script>-->
<script src="./source_docs.js"></script> <script src="./source_docs.js"></script>
<script src="./source_doc_urls.js"></script>
<script src="./source.js"></script> <script src="./source.js"></script>
<script src="./dev_bridge.js"></script> <script src="./dev_bridge.js"></script>
<script> <script>
@ -556,7 +732,9 @@
lastLogIndex: -1, lastLogIndex: -1,
lastLogDevID: "", lastLogDevID: "",
logs: [], logs: [],
lastInjectTime: "" httpExchanges: [],
lastInjectTime: "",
showHttpRequests: false
}, },
Plugin: { Plugin: {
loadUsingTag: false, loadUsingTag: false,
@ -574,6 +752,9 @@
Testing: { Testing: {
requests: sourceDocs.map(x=>{ requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null); x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x; return x;
}), }),
lastResult: "", lastResult: "",
@ -637,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) { catch(ex) {
console.error("Failed update", ex); console.error("Failed update", ex);
@ -678,6 +869,12 @@
this.reloadPlugin(); 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() { loginTestPlugin() {
pluginLoginTestPlugin(); pluginLoginTestPlugin();
setTimeout(()=>{ setTimeout(()=>{
@ -913,6 +1110,9 @@
}, },
showTestResults(results) { showTestResults(results) {
},
toggleHttpExchange(exchange) {
exchange.response.show = !exchange.response.show;
}, },
copyClipboard(cpy) { copyClipboard(cpy) {
if(navigator.clipboard) if(navigator.clipboard)

View file

@ -127,7 +127,7 @@ declare class PlatformVideoDetails extends PlatformVideo {
} }
declare interface PlatformPostDef extends PlatformContentDef { declare interface PlatformPostDef extends PlatformContentDef {
thumbnails: string[], thumbnails: Thumbnails[],
images: string[], images: string[],
description: string description: string
} }

File diff suppressed because one or more lines are too long

View file

@ -11,7 +11,8 @@ let Type = {
Streams: "STREAMS", Streams: "STREAMS",
Mixed: "MIXED", Mixed: "MIXED",
Live: "LIVE", Live: "LIVE",
Subscriptions: "SUBSCRIPTIONS" Subscriptions: "SUBSCRIPTIONS",
Shorts: "SHORTS"
}, },
Order: { Order: {
Chronological: "CHRONOLOGICAL" Chronological: "CHRONOLOGICAL"
@ -201,7 +202,7 @@ class PlatformContent {
obj = obj ?? {}; obj = obj ?? {};
this.id = obj.id ?? PlatformID(); //PlatformID this.id = obj.id ?? PlatformID(); //PlatformID
this.name = obj.name ?? ""; //string this.name = obj.name ?? ""; //string
this.thumbnails = obj.thumbnails; //Thumbnail[] this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
this.author = obj.author; //PlatformAuthorLink this.author = obj.author; //PlatformAuthorLink
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long) this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
this.url = obj.url ?? ""; //String this.url = obj.url ?? ""; //String
@ -244,6 +245,7 @@ class PlatformVideo extends PlatformContent {
this.viewCount = obj.viewCount ?? -1; //Long this.viewCount = obj.viewCount ?? -1; //Long
this.isLive = obj.isLive ?? false; //Boolean this.isLive = obj.isLive ?? false; //Boolean
this.isShort = !!obj.isShort ?? false;
} }
} }
class PlatformVideoDetails extends PlatformVideo { class PlatformVideoDetails extends PlatformVideo {
@ -260,6 +262,11 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? []; this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
} }
} }
@ -278,12 +285,49 @@ class PlatformPostDetails extends PlatformPost {
super(obj); super(obj);
obj = obj ?? {}; obj = obj ?? {};
this.plugin_type = "PlatformPostDetails"; this.plugin_type = "PlatformPostDetails";
this.rating = obj.rating ?? RatingLikes(-1); this.rating = obj.rating ?? new RatingLikes(-1);
this.textType = obj.textType ?? 0; this.textType = obj.textType ?? 0;
this.content = obj.content ?? ""; this.content = obj.content ?? "";
} }
} }
class PlatformArticleDetails extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class ArticleSegment {
constructor(type) {
this.type = type;
}
}
class ArticleTextSegment extends ArticleSegment {
constructor(content, textType) {
super(1);
this.textType = textType;
this.content = content;
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
super(2);
this.images = images;
}
}
class ArticleNestedSegment extends ArticleSegment {
constructor(nested) {
super(9);
this.nested = nested;
}
}
//Sources //Sources
class VideoSourceDescriptor { class VideoSourceDescriptor {
constructor(obj) { constructor(obj) {
@ -330,6 +374,16 @@ class VideoUrlSource {
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
} }
} }
class VideoUrlWidevineSource extends VideoUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "VideoUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class VideoUrlRangeSource extends VideoUrlSource { class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj) { constructor(obj) {
super(obj); super(obj);
@ -357,6 +411,33 @@ class AudioUrlSource {
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
} }
} }
class AudioUrlWidevineSource extends AudioUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "AudioUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
// deprecated api conversion
if(obj.bearerToken) {
this.getLicenseRequestExecutor = () => {
return {
executeRequest: (url, _headers, _method, license_request_data) => {
return http.POST(
url,
license_request_data,
{ Authorization: `Bearer ${obj.bearerToken}` },
false,
true
).body
}
}
}
}
}
}
class AudioUrlRangeSource extends AudioUrlSource { class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj) { constructor(obj) {
super(obj); super(obj);
@ -397,6 +478,49 @@ class DashSource {
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
} }
} }
class DashWidevineSource extends DashSource {
constructor(obj) {
super(obj);
this.plugin_type = "DashWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class DashManifestRawSource {
constructor(obj) {
obj = obj ?? {};
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 { class RequestModifier {
constructor(obj) { constructor(obj) {
@ -427,7 +551,7 @@ class PlatformPlaylist extends PlatformContent {
constructor(obj) { constructor(obj) {
super(obj, 4); super(obj, 4);
this.plugin_type = "PlatformPlaylist"; this.plugin_type = "PlatformPlaylist";
this.videoCount = obj.videoCount ?? 0; this.videoCount = obj.videoCount ?? -1;
this.thumbnail = obj.thumbnail; this.thumbnail = obj.thumbnail;
} }
} }
@ -753,3 +877,99 @@ class URLSearchParams {
return searchString; return searchString;
} }
} }
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function btoa(input) {
input = String(input);
if (/[^\0-\xFF]/.test(input)) {
// Note: no need to special-case astral symbols here, as surrogates are
// matched, and the input is supposed to only contain ASCII anyway.
error(
'The string to be encoded contains characters outside of the ' +
'Latin1 range.'
);
}
var padding = input.length % 3;
var output = '';
var position = -1;
var a;
var b;
var c;
var buffer;
// Make sure any padding is handled outside of the loop.
var length = input.length - padding;
while (++position < length) {
// Read three bytes, i.e. 24 bits.
a = input.charCodeAt(position) << 16;
b = input.charCodeAt(++position) << 8;
c = input.charCodeAt(++position);
buffer = a + b + c;
// Turn the 24 bits into four chunks of 6 bits each, and append the
// matching character for each of them to the output.
output += (
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
__btoa_TABLE.charAt(buffer & 0x3F)
);
}
if (padding == 2) {
a = input.charCodeAt(position) << 8;
b = input.charCodeAt(++position);
buffer = a + b;
output += (
__btoa_TABLE.charAt(buffer >> 10) +
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
'='
);
} else if (padding == 1) {
buffer = input.charCodeAt(position);
output += (
__btoa_TABLE.charAt(buffer >> 2) +
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
'=='
);
}
return output;
};
function atob(input) {
input = String(input)
.replace(__REGEX_SPACE_CHARACTERS, '');
var length = input.length;
if (length % 4 == 0) {
input = input.replace(/==?$/, '');
length = input.length;
}
if (
length % 4 == 1 ||
// http://whatwg.org/C#alphanumeric-ascii-characters
/[^+a-zA-Z0-9/]/.test(input)
) {
error(
'Invalid character: the string to be decoded is not correctly encoded.'
);
}
var bitCounter = 0;
var bitStorage;
var buffer;
var output = '';
var position = -1;
while (++position < length) {
buffer = __btoa_TABLE.indexOf(input.charAt(position));
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
// Unless this is the first of a group of 4 characters…
if (bitCounter++ % 4) {
// …convert the first 8 bits to a single ASCII character.
output += String.fromCharCode(
0xFF & bitStorage >> (-2 * bitCounter & 6)
);
}
}
return output;
};

View file

@ -18,7 +18,10 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi @UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory { fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier(); val requestModifier = getRequestModifier();
return if (requestModifier != null) { val requestExecutor = getRequestExecutor();
return if (requestExecutor != null) {
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
} else if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier); JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else { } else {
DefaultHttpDataSource.Factory(); DefaultHttpDataSource.Factory();

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.Inet4Address import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
@ -215,9 +216,14 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this); return InetAddress.getByAddress(this);
} }
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000 val timeout = 2000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return null; return null;
} }

View file

@ -1,13 +1,13 @@
package com.futo.platformplayer package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol import userpackage.Protocol
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -40,36 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) } return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
} }
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
val urlData = if (this.startsWith("polycentric://")) {
this.substring("polycentric://".length)
} else this;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
fun Protocol.Claim.resolveChannelUrl(): String? { fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
} }
fun Protocol.Claim.resolveChannelUrls(): List<String> { fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) 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.STAGING_SERVER)) {
removeServer(PolycentricCache.STAGING_SERVER)
}
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
removeServer(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)
}
} }

View file

@ -1,6 +1,9 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.net.Uri import android.net.Uri
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URLEncoder import java.net.URLEncoder
@ -25,4 +28,18 @@ fun String?.yesNoToBoolean(): Boolean {
fun Boolean?.toYesNo(): String { fun Boolean?.toYesNo(): String {
return if (this == true) "YES" else "NO" return if (this == true) "YES" else "NO"
}
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
"[${hostAddress}]"
}
is Inet4Address -> {
hostAddress
}
else -> {
throw Exception("Invalid address type")
}
}
} }

View file

@ -0,0 +1,192 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.io.ByteStreams
import com.google.common.primitives.Ints
import com.google.common.primitives.Longs
import java.io.DataInput
import java.io.DataInputStream
import java.io.EOFException
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
class LittleEndianDataInputStream
/**
* Creates a `LittleEndianDataInputStream` that wraps the given stream.
*
* @param in the stream to delegate to
*/
(`in`: InputStream?) : FilterInputStream(Preconditions.checkNotNull(`in`)), DataInput {
/** This method will throw an [UnsupportedOperationException]. */
override fun readLine(): String {
throw UnsupportedOperationException("readLine is not supported")
}
@Throws(IOException::class)
override fun readFully(b: ByteArray) {
ByteStreams.readFully(this, b)
}
@Throws(IOException::class)
override fun readFully(b: ByteArray, off: Int, len: Int) {
ByteStreams.readFully(this, b, off, len)
}
@Throws(IOException::class)
override fun skipBytes(n: Int): Int {
return `in`.skip(n.toLong()).toInt()
}
@Throws(IOException::class)
override fun readUnsignedByte(): Int {
val b1 = `in`.read()
if (0 > b1) {
throw EOFException()
}
return b1
}
/**
* Reads an unsigned `short` as specified by [DataInputStream.readUnsignedShort],
* except using little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as an unsigned 16-bit integer in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readUnsignedShort(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
return Ints.fromBytes(0.toByte(), 0.toByte(), b2, b1)
}
/**
* Reads an integer as specified by [DataInputStream.readInt], except using little-endian
* byte order.
*
* @return the next four bytes of the input stream, interpreted as an `int` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readInt(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
return Ints.fromBytes(b4, b3, b2, b1)
}
/**
* Reads a `long` as specified by [DataInputStream.readLong], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `long` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readLong(): Long {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
val b5 = readAndCheckByte()
val b6 = readAndCheckByte()
val b7 = readAndCheckByte()
val b8 = readAndCheckByte()
return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1)
}
/**
* Reads a `float` as specified by [DataInputStream.readFloat], except using
* little-endian byte order.
*
* @return the next four bytes of the input stream, interpreted as a `float` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readFloat(): Float {
return java.lang.Float.intBitsToFloat(readInt())
}
/**
* Reads a `double` as specified by [DataInputStream.readDouble], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `double` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readDouble(): Double {
return java.lang.Double.longBitsToDouble(readLong())
}
@Throws(IOException::class)
override fun readUTF(): String {
return DataInputStream(`in`).readUTF()
}
/**
* Reads a `short` as specified by [DataInputStream.readShort], except using
* little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as a `short` in little-endian
* byte order.
* @throws IOException if an I/O error occurs.
*/
@Throws(IOException::class)
override fun readShort(): Short {
return readUnsignedShort().toShort()
}
/**
* Reads a char as specified by [DataInputStream.readChar], except using little-endian
* byte order.
*
* @return the next two bytes of the input stream, interpreted as a `char` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readChar(): Char {
return readUnsignedShort().toChar()
}
@Throws(IOException::class)
override fun readByte(): Byte {
return readUnsignedByte().toByte()
}
@Throws(IOException::class)
override fun readBoolean(): Boolean {
return readUnsignedByte() != 0
}
/**
* Reads a byte from the input stream checking that the end of file (EOF) has not been
* encountered.
*
* @return byte read from input
* @throws IOException if an error is encountered while reading
* @throws EOFException if the end of file (EOF) is encountered.
*/
@Throws(IOException::class, EOFException::class)
private fun readAndCheckByte(): Byte {
val b1 = `in`.read()
if (-1 == b1) {
throw EOFException()
}
return b1.toByte()
}
}

View file

@ -0,0 +1,144 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.primitives.Longs
import java.io.*
class LittleEndianDataOutputStream
/**
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
*
* @param out the stream to delegate to
*/
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
DataOutput {
@Throws(IOException::class)
override fun write(b: ByteArray, off: Int, len: Int) {
// Override slow FilterOutputStream impl
out.write(b, off, len)
}
@Throws(IOException::class)
override fun writeBoolean(v: Boolean) {
(out as DataOutputStream).writeBoolean(v)
}
@Throws(IOException::class)
override fun writeByte(v: Int) {
(out as DataOutputStream).writeByte(v)
}
@Deprecated(
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
)
@Throws(
IOException::class
)
override fun writeBytes(s: String) {
(out as DataOutputStream).writeBytes(s)
}
/**
* Writes a char as specified by [DataOutputStream.writeChar], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChar(v: Int) {
writeShort(v)
}
/**
* Writes a `String` as specified by [DataOutputStream.writeChars], except
* each character is written using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChars(s: String) {
for (i in 0 until s.length) {
writeChar(s[i].code)
}
}
/**
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
* using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeDouble(v: Double) {
writeLong(java.lang.Double.doubleToLongBits(v))
}
/**
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeFloat(v: Float) {
writeInt(java.lang.Float.floatToIntBits(v))
}
/**
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeInt(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte(),
(0xFF and (v shr 16)).toByte(),
(0xFF and (v shr 24)).toByte()
)
out.write(bytes)
}
/**
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeLong(v: Long) {
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
write(bytes, 0, bytes.size)
}
/**
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeShort(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte()
)
out.write(bytes)
}
@Throws(IOException::class)
override fun writeUTF(str: String) {
(out as DataOutputStream).writeUTF(str)
}
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
// It should flush itself if necessary.
@Throws(IOException::class)
override fun close() {
out.close()
}
}

View file

@ -11,6 +11,7 @@ import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.SyncHomeActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@ -32,7 +33,6 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,6 +44,7 @@ import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@Serializable @Serializable
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean); data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
@ -57,7 +58,16 @@ class Settings : FragmentedStorageFileJson() {
@Transient @Transient
val onTabsChanged = Event0(); val onTabsChanged = Event0();
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6) @FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
@FormFieldButton(R.drawable.ic_update)
fun syncGrayjay() {
SettingsActivity.getActivity()?.let {
it.startActivity(Intent(it, SyncHomeActivity::class.java))
}
}
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
@FormFieldButton(R.drawable.ic_person) @FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() { fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
@ -73,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5) @FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
@FormFieldButton(R.drawable.ic_quiz) @FormFieldButton(R.drawable.ic_quiz)
fun openFAQ() { fun openFAQ() {
try { try {
@ -83,7 +93,7 @@ class Settings : FragmentedStorageFileJson() {
//Ignored //Ignored
} }
} }
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4) @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) @FormFieldButton(R.drawable.ic_data_alert)
fun openIssues() { fun openIssues() {
try { try {
@ -115,7 +125,7 @@ class Settings : FragmentedStorageFileJson() {
} }
}*/ }*/
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3) @FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
@FormFieldButton(R.drawable.ic_tabs) @FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() { fun manageTabs() {
try { try {
@ -129,16 +139,15 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2) @FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
@FormFieldButton(R.drawable.ic_move_up) @FormFieldButton(R.drawable.ic_move_up)
fun import() { fun import() {
val act = SettingsActivity.getActivity() ?: return; val act = SettingsActivity.getActivity() ?: return;
val intent = MainActivity.getImportOptionsIntent(act); val intent = MainActivity.getImportOptionsIntent(act);
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
act.startActivity(intent); act.startActivity(intent);
} }
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1) @FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
@FormFieldButton(R.drawable.ic_link) @FormFieldButton(R.drawable.ic_link)
fun manageLinks() { fun manageLinks() {
try { try {
@ -148,6 +157,24 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
/*@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) @FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings(); var language = LanguageSettings();
@Serializable @Serializable
@ -178,7 +205,7 @@ class Settings : FragmentedStorageFileJson() {
var home = HomeSettings(); var home = HomeSettings();
@Serializable @Serializable
class HomeSettings { class HomeSettings {
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5) @FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
@DropdownFieldOptionsId(R.array.feed_style) @DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1; var homeFeedStyle: Int = 1;
@ -189,6 +216,11 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL; return FeedStyle.THUMBNAIL;
} }
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
var showHomeFilters: Boolean = true;
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@ -227,6 +259,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
var hidefromSearch: Boolean = false;
fun getSearchFeedStyle(): FeedStyle { fun getSearchFeedStyle(): FeedStyle {
if(searchFeedStyle == 0) if(searchFeedStyle == 0)
@ -264,6 +299,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5) @FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true; var showSubscriptionGroups: Boolean = true;
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@ -311,7 +349,10 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15) @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() { fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing.."); UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear(); StateCache.instance.clear();
@ -323,7 +364,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings(); var playback = PlaybackSettings();
@Serializable @Serializable
class PlaybackSettings { class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0) @FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
@DropdownFieldOptionsId(R.array.audio_languages) @DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0; var primaryLanguage: Int = 0;
@ -347,10 +388,12 @@ class Settings : FragmentedStorageFileJson() {
else -> null else -> null
} }
} }
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true;
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage]; //= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1) @FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.playback_speeds) @DropdownFieldOptionsId(R.array.playback_speeds)
var defaultPlaybackSpeed: Int = 3; var defaultPlaybackSpeed: Int = 3;
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) { fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
@ -366,37 +409,29 @@ class Settings : FragmentedStorageFileJson() {
else -> 1.0f; else -> 1.0f;
}; };
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2) @FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredQuality: Int = 0; var preferredQuality: Int = 0;
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3) @FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredMeteredQuality: Int = 0; var preferredMeteredQuality: Int = 0;
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality); fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality); fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount(); fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4) @FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5) @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array) var simplifySources: Boolean = true;
var autoRotate: Int = 2;
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate()); @FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6) @FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
var autoRotateDeadZone: Int = 0;
fun getAutoRotateDeadZoneDegrees(): Int {
return autoRotateDeadZone * 5;
}
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@DropdownFieldOptionsId(R.array.player_background_behavior) @DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2; var backgroundPlay: Int = 2;
@ -447,18 +482,44 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13) @FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false; var fullscreenPortrait: Boolean = false;
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
var allowVideoToGoUnderCutout: Boolean = true;
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)
var comments = CommentSettings(); var comments = CommentSettings();
@Serializable @Serializable
class CommentSettings { class CommentSettings {
var didAskPolycentricDefault: Boolean = false;
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0) @FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections) @DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 0; var defaultCommentSection: Int = 2;
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0) @FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true; var badReputationCommentsFading: Boolean = true;
} }
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7) @FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
@ -507,7 +568,7 @@ class Settings : FragmentedStorageFileJson() {
class Browsing { class Browsing {
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.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) @Serializable(with = FlexibleBooleanSerializer::class)
var videoCache: Boolean = true; var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
} }
@FormField(R.string.casting, "group", R.string.configure_casting, 9) @FormField(R.string.casting, "group", R.string.configure_casting, 9)
@ -522,6 +583,15 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = false;
/*TODO: Should we have a different casting quality? /*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
@ -546,6 +616,8 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.log_levels) @DropdownFieldOptionsId(R.array.log_levels)
var logLevel: Int = 0; var logLevel: Int = 0;
fun isVerbose() = logLevel >= 4;
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1) @FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
fun submitLogs() { fun submitLogs() {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
@ -587,6 +659,9 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;
@ -770,10 +845,10 @@ class Settings : FragmentedStorageFileJson() {
fun export() { fun export() {
val activity = SettingsActivity.getActivity() ?: return; val activity = SettingsActivity.getActivity() ?: return;
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {}, UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, { SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
StateBackup.shareExternalBackup(); StateBackup.shareExternalBackup();
}), }),
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, { SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
StateBackup.saveExternalBackup(activity); StateBackup.saveExternalBackup(activity);
}) })
) )
@ -789,10 +864,14 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2) @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun clearPayment() { fun clearPayment() {
StatePayment.instance.clearLicenses(); SettingsActivity.getActivity()?.let { context ->
SettingsActivity.getActivity()?.let { UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart)); StatePayment.instance.clearLicenses();
it.reloadSettings(); SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
it.reloadSettings();
}
})
} }
} }
} }
@ -801,12 +880,16 @@ class Settings : FragmentedStorageFileJson() {
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1) @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
@FormFieldWarning(R.string.bypass_rotation_prevention_warning) var playlistDeleteConfirmation: Boolean = true;
var bypassRotationPrevention: Boolean = false; @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1) @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
var polycentricLocalCache: Boolean = true;
} }
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19) @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@ -838,7 +921,33 @@ class Settings : FragmentedStorageFileJson() {
var pan: Boolean = true; var pan: Boolean = true;
} }
@FormField(R.string.info, FieldForm.GROUP, -1, 20) @FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
var synchronization = Synchronization();
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = true;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false;
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
var connectDiscovered: Boolean = true;
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true;
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
var discoverThroughRelay: Boolean = true;
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
var pairThroughRelay: Boolean = true;
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
var connectThroughRelay: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
var info = Info(); var info = Info();
@Serializable @Serializable
class Info { class Info {

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.content.Context import android.content.Context
import android.content.Intent
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.work.Data import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
@ -8,6 +9,7 @@ import androidx.work.WorkManager
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity import com.futo.platformplayer.activities.DeveloperActivity
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -33,6 +35,7 @@ import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.fields.ButtonField import com.futo.platformplayer.views.fields.ButtonField
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -233,13 +236,17 @@ class SettingsDev : FragmentedStorageFileJson() {
R.string.test_background_worker_description, 4) R.string.test_background_worker_description, 4)
fun triggerBackgroundUpdate() { fun triggerBackgroundUpdate() {
val act = SettingsActivity.getActivity()!!; val act = SettingsActivity.getActivity()!!;
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker"); try {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
val wm = WorkManager.getInstance(act); val wm = WorkManager.getInstance(act);
val req = OneTimeWorkRequestBuilder<BackgroundWorker>() val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build()) .setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
.build(); .build();
wm.enqueue(req); wm.enqueue(req);
} catch (e: Throwable) {
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
}
} }
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, @FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 4) R.string.test_background_worker_description, 4)
@ -490,6 +497,24 @@ 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;
} }
@ -503,6 +528,8 @@ class SettingsDev : FragmentedStorageFileJson() {
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount; var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
} }
//region BOILERPLATE //region BOILERPLATE
override fun encode(): String { override fun encode(): String {
return Json.encodeToString(this); return Json.encodeToString(this);

View file

@ -5,7 +5,10 @@ import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@ -18,6 +21,7 @@ import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.comments.IPlatformComment 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.casting.StateCasting
import com.futo.platformplayer.dialogs.AutoUpdateDialog import com.futo.platformplayer.dialogs.AutoUpdateDialog
import com.futo.platformplayer.dialogs.AutomaticBackupDialog import com.futo.platformplayer.dialogs.AutomaticBackupDialog
@ -31,11 +35,17 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
import com.futo.platformplayer.dialogs.ImportDialog import com.futo.platformplayer.dialogs.ImportDialog
import com.futo.platformplayer.dialogs.ImportOptionsDialog import com.futo.platformplayer.dialogs.ImportOptionsDialog
import com.futo.platformplayer.dialogs.MigrateDialog import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.PluginUpdateDialog
import com.futo.platformplayer.dialogs.ProgressDialog import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException 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.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -183,43 +193,58 @@ class UIDialogs {
dialog.show(); dialog.show();
} }
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) { 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): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context); val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view); builder.setView(view);
builder.setCancelable(defaultCloseAction > -2);
val dialog = builder.create(); val dialog = builder.create();
registerDialogOpened(dialog); registerDialogOpened(dialog);
view.findViewById<ImageView>(R.id.dialog_icon).apply { view.findViewById<ImageView>(R.id.dialog_icon).apply {
this.setImageResource(icon); this.setImageResource(icon);
if(animated)
this.drawable.assume<Animatable, Unit> { it.start() };
} }
view.findViewById<TextView>(R.id.dialog_text).apply { view.findViewById<TextView>(R.id.dialog_text).apply {
this.text = text; this.text = text;
}; };
view.findViewById<TextView>(R.id.dialog_text_details).apply { view.findViewById<TextView>(R.id.dialog_text_details).apply {
if(textDetails == null) if (textDetails == null)
this.visibility = View.GONE;
else
this.text = textDetails;
};
view.findViewById<TextView>(R.id.dialog_text_code).apply {
if(code == null)
this.visibility = View.GONE; this.visibility = View.GONE;
else {
this.text = textDetails;
}
};
view.findViewById<TextView>(R.id.dialog_text_code).apply {
if (code == null) this.visibility = View.GONE;
else { else {
this.text = code; this.text = code;
this.movementMethod = ScrollingMovementMethod.getInstance();
this.visibility = View.VISIBLE; this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
} }
}; };
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply { view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
val center = actions.any { it?.center == true };
val buttons = actions.map<Action, TextView> { act -> val buttons = actions.map<Action, TextView> { act ->
val buttonView = TextView(context); val buttonView = TextView(context);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt(); val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt(); val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt(); val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
if(actions.size > 1) this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
this.marginEnd = if(actions.size > 2) dp14 else dp28; this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
}; };
buttonView.setTextColor(Color.WHITE); buttonView.setTextColor(Color.WHITE);
buttonView.textSize = 14f; buttonView.textSize = 14f;
@ -241,7 +266,7 @@ class UIDialogs {
return@map buttonView; return@map buttonView;
}; };
if(actions.size <= 1) if(actions.size <= 1 || center)
this.gravity = Gravity.CENTER; this.gravity = Gravity.CENTER;
else else
this.gravity = Gravity.END; this.gravity = Gravity.END;
@ -256,6 +281,7 @@ class UIDialogs {
registerDialogClosed(dialog); registerDialogClosed(dialog);
} }
dialog.show(); dialog.show();
return dialog;
} }
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) { fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
@ -268,22 +294,48 @@ class UIDialogs {
}, UIDialogs.ActionStyle.PRIMARY) }, 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) val pluginInfo = if(ex is PluginException)
"\nPlugin [${ex.config.name}]" else ""; "\nPlugin [${ex.config.name}]" else "";
showDialog(context,
R.drawable.ic_error_pred, var exMsg = if(ex != null ) "${ex.message}" else "";
"${msg}${pluginInfo}", if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
(if(ex != null ) "${ex.message}" else ""), exMsg += "\n\nAn update is available"
if(ex is PluginException) ex.code else null,
0, if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
UIDialogs.Action(context.getString(R.string.retry), { showDialog(context,
retryAction?.invoke(); R.drawable.ic_error_pred,
}, UIDialogs.ActionStyle.PRIMARY), "${msg}${pluginInfo}",
UIDialogs.Action(context.getString(R.string.close), { exMsg,
closeAction?.invoke() if(ex is PluginException) ex.code else null,
}, UIDialogs.ActionStyle.NONE) 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)) { fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
@ -304,6 +356,13 @@ class UIDialogs {
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction) showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
} }
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
}
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) { fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
val dialog = AutoUpdateDialog(context); val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
@ -316,8 +375,8 @@ class UIDialogs {
} }
} }
fun showChangelogDialog(context: Context, lastVersion: Int) { fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
val dialog = ChangelogDialog(context); val dialog = ChangelogDialog(context, changelogs);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
@ -343,8 +402,8 @@ class UIDialogs {
} }
} }
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) { fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded); val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
@ -466,11 +525,13 @@ class UIDialogs {
val text: String; val text: String;
val action: ()->Unit; val action: ()->Unit;
val style: ActionStyle; val style: ActionStyle;
var center: Boolean;
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) { constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text; this.text = text;
this.action = action; this.action = action;
this.style = style; this.style = style;
this.center = center;
} }
} }
enum class ActionStyle { enum class ActionStyle {

View file

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
@ -14,14 +15,19 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
@ -33,15 +39,21 @@ import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.futo.platformplayer.views.pills.RoundButton import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup import com.futo.platformplayer.views.pills.RoundButtonGroup
@ -67,6 +79,36 @@ class UISlideOverlays {
return menu; return menu;
} }
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
UISlideOverlays.showOverlay(container, "Queue options", null, {
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text.trim()
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
StatePlayer.instance.saveQueueAsPlaylist(text);
UIDialogs.appToast("Playlist [${text}] created");
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
}, false));
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay { fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
@ -84,29 +126,107 @@ class UISlideOverlays {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
items.addAll(listOf( items.addAll(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", { SlideUpMenuItem(
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications; container.context,
}, false), R.drawable.ic_notifications,
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
},
invokeParent = false
),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()) else null,
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuRecycler(container.context, "as") {
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if(it is SubscriptionGroup.Selectable) {
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
?: return@subscribe;
groups.clear();
if(it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(container.context, "Fetch Settings", SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.", "Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()), -1, listOf()),
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", { if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive; container.context,
}, false) else null, R.drawable.ic_live_tv,
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", { "Livestreams",
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams; "Check for livestreams",
}, false) else null, tag = "fetchLive",
call = {
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Streams",
"Check for streams",
tag = "fetchStreams",
call = {
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS)) if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", { SlideUpMenuItem(
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos; container.context,
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty()) R.drawable.ic_play,
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", { "Videos",
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos; "Check for videos",
}, false) else null, tag = "fetchVideos",
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", { call = {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts; subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
}, false) else null/*,, },
invokeParent = false
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Content",
"Check for content",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
SlideUpMenuGroup(container.context, "Actions", SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription", "Various things you can do with this subscription",
@ -205,11 +325,23 @@ class UISlideOverlays {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
masterPlaylist.getAudioSources().forEach { it -> masterPlaylist.getAudioSources().forEach { it ->
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedAudioVariant = it val estSize = VideoHelper.estimateSourceSize(it);
slideUpMenuOverlay.selectOption(audioButtons, it) val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) audioButtons.add(SlideUpMenuItem(
}, false)) container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
} }
/*masterPlaylist.getSubtitleSources().forEach { it -> /*masterPlaylist.getSubtitleSources().forEach { it ->
@ -221,11 +353,24 @@ class UISlideOverlays {
}*/ }*/
masterPlaylist.getVideoSources().forEach { masterPlaylist.getVideoSources().forEach {
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, { val estSize = VideoHelper.estimateSourceSize(it);
selectedVideoVariant = it val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
slideUpMenuOverlay.selectOption(videoButtons, it) videoButtons.add(SlideUpMenuItem(
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) container.context,
}, false)) R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
} }
val newItems = arrayListOf<View>() val newItems = arrayListOf<View>()
@ -257,7 +402,7 @@ class UISlideOverlays {
UIDialogs.toast(container.context, "Variant video HLS playlist download started") UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) { } else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null) StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started") UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else { } else {
@ -284,8 +429,8 @@ class UISlideOverlays {
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor; val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
var selectedVideo: IVideoUrlSource? = null; var selectedVideo: IVideoSource? = null;
var selectedAudio: IAudioUrlSource? = null; var selectedAudio: IAudioSource? = null;
var selectedSubtitle: ISubtitleSource? = null; var selectedSubtitle: ISubtitleSource? = null;
val videoSources = descriptor.videoSources; val videoSources = descriptor.videoSources;
@ -304,45 +449,93 @@ class UISlideOverlays {
} }
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources, items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", { listOf((if (audioSources != null) listOf(SlideUpMenuItem(
selectedVideo = null; container.context,
menu?.selectOption(videoSources, "none"); R.drawable.ic_movie,
if(selectedAudio != null || !requiresAudio) container.context.getString(R.string.none),
menu?.setOk(container.context.getString(R.string.download)); container.context.getString(R.string.audio_only),
}, false)) + tag = "none",
call = {
selectedVideo = null;
menu?.selectOption(videoSources, "none");
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)) else listOf()) +
videoSources videoSources
.filter { it.isDownloadable() } .filter { it.isDownloadable() }
.map { .map {
when (it) { when (it) {
is IVideoUrlSource -> { is IVideoUrlSource -> {
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, { val estSize = VideoHelper.estimateSourceSize(it);
selectedVideo = it val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
menu?.selectOption(videoSources, it); SlideUpMenuItem(
if(selectedAudio != null || !requiresAudio) container.context,
menu?.setOk(container.context.getString(R.string.download)); R.drawable.ic_movie,
}, false) it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
}
is JSDashManifestRawSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
} }
is IHLSManifestSource -> { is IHLSManifestSource -> {
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, { SlideUpMenuItem(
showHlsPicker(video, it, it.url, container) container.context,
}, false) R.drawable.ic_movie,
it.name,
"HLS",
tag = it,
call = {
showHlsPicker(video, it, it.url, container)
},
invokeParent = false
)
} }
else -> { else -> {
throw Exception("Unhandled source type") Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
null;//throw Exception("Unhandled source type")
} }
} }
}).flatten().toList() }.filterNotNull()).flatten().toList()
)); ));
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) { if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
//TODO: Add HLS support here //TODO: Add HLS support here
selectedVideo = VideoHelper.selectBestVideoSource( selectedVideo = VideoHelper.selectBestVideoSource(
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(), videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(), Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
) as IVideoUrlSource?; ) as IVideoSource?;
} }
if (audioSources != null) { if (audioSources != null) {
@ -351,43 +544,90 @@ class UISlideOverlays {
.map { .map {
when (it) { when (it) {
is IAudioUrlSource -> { is IAudioUrlSource -> {
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, { val estSize = VideoHelper.estimateSourceSize(it);
selectedAudio = it val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
menu?.selectOption(audioSources, it); SlideUpMenuItem(
menu?.setOk(container.context.getString(R.string.download)); container.context,
}, false); R.drawable.ic_music,
it.name,
"${it.bitrate}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
);
}
is JSDashManifestRawAudioSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
"${it.bitrate}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
);
} }
is IHLSManifestAudioSource -> { is IHLSManifestAudioSource -> {
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, { SlideUpMenuItem(
showHlsPicker(video, it, it.url, container) container.context,
}, false) R.drawable.ic_movie,
it.name,
"HLS Audio",
tag = it,
call = {
showHlsPicker(video, it, it.url, container)
},
invokeParent = false
)
} }
else -> { else -> {
throw Exception("Unhandled source type") Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
null;//throw Exception("Unhandled source type")
} }
} }
})); }.filterNotNull()));
//TODO: Add HLS support here //TODO: Add HLS support here
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(), selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
Settings.instance.playback.getPrimaryLanguage(container.context), Settings.instance.playback.getPrimaryLanguage(container.context),
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?; if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
} }
if(contentResolver != null && subtitleSources.isNotEmpty()) { if(contentResolver != null && subtitleSources.isNotEmpty()) {
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map { items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, { SlideUpMenuItem(
if (selectedSubtitle == it) { container.context,
selectedSubtitle = null; R.drawable.ic_edit,
menu?.selectOption(subtitleSources, null); it.name,
} else { "",
selectedSubtitle = it; tag = it,
menu?.selectOption(subtitleSources, it); call = {
} if (selectedSubtitle == it) {
}, false); selectedSubtitle = null;
menu?.selectOption(subtitleSources, null);
} else {
selectedSubtitle = it;
menu?.selectOption(subtitleSources, it);
}
},
invokeParent = false
);
}) })
); );
} }
@ -405,6 +645,18 @@ class UISlideOverlays {
} }
menu.onOK.subscribe { menu.onOK.subscribe {
val sv = selectedVideo
if (sv is IHLSManifestSource) {
showHlsPicker(video, sv, sv.url, container)
return@subscribe
}
val sa = selectedAudio
if (sa is IHLSManifestAudioSource) {
showHlsPicker(video, sa, sa.url, container)
return@subscribe
}
menu.hide(); menu.hide();
val subtitleToDownload = selectedSubtitle; val subtitleToDownload = selectedSubtitle;
if(selectedAudio != null || !requiresAudio) { if(selectedAudio != null || !requiresAudio) {
@ -461,8 +713,9 @@ class UISlideOverlays {
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download)); UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
handleUnknownDownload(); handleUnknownDownload();
loader.hide(true); loader.hide(true);
} }
@ -473,10 +726,15 @@ class UISlideOverlays {
} }
} }
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) { fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate -> showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
StateDownloads.instance.download(playlist, px, bitrate); StateDownloads.instance.download(playlist, px, bitrate);
}; };
} }
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
StateDownloads.instance.downloadWatchLater(px, bitrate);
})
}
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) { private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
var menu: SlideUpMenuOverlay? = null; var menu: SlideUpMenuOverlay? = null;
@ -494,23 +752,47 @@ class UISlideOverlays {
); );
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map { items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, { SlideUpMenuItem(
targetPxSize = it.third; container.context,
menu?.selectOption("Video", it.third); R.drawable.ic_movie,
}, false) it.first,
it.second,
tag = it.third,
call = {
targetPxSize = it.third;
menu?.selectOption("Video", it.third);
},
invokeParent = false
)
})); }));
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf( items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, { SlideUpMenuItem(
targetBitrate = 1; container.context,
menu?.selectOption("Bitrate", 1); R.drawable.ic_movie,
menu?.setOk(container.context.getString(R.string.download)); container.context.getString(R.string.low_bitrate),
}, false), "",
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, { tag = 1,
targetBitrate = 9999999; call = {
menu?.selectOption("Bitrate", 9999999); targetBitrate = 1;
menu?.setOk(container.context.getString(R.string.download)); menu?.selectOption("Bitrate", 1);
}, false) menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
),
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.high_bitrate),
"",
tag = 9999999,
call = {
targetBitrate = 9999999;
menu?.selectOption("Bitrate", 9999999);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
))); )));
@ -630,12 +912,23 @@ class UISlideOverlays {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist(); val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
if (it is JSClient)
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
else false;
} ?: false;
if (lastUpdated != null) { if (lastUpdated != null) {
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist", SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "", SlideUpMenuItem(container.context,
{ R.drawable.ic_playlist_add,
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video); lastUpdated.name,
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})) }))
); );
@ -646,35 +939,93 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions", items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf( (listOf(
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), { if(!isLimited && !video.isLive)
showDownloadVideoOverlay(video, container, true); SlideUpMenuItem(
}, false), container.context,
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", { R.drawable.ic_download,
StateMeta.instance.addHiddenCreator(video.author.url); container.context.getString(R.string.download),
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home"); container.context.getString(R.string.download_the_video),
})) tag = "download",
+ actions) call = {
showDownloadVideoOverlay(video, container, true);
},
invokeParent = false
) else null,
SlideUpMenuItem(
container.context,
R.drawable.ic_share,
container.context.getString(R.string.share),
"Share the video",
tag = "share",
call = {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
},
invokeParent = false
),
SlideUpMenuItem(
container.context,
R.drawable.ic_visibility_off,
container.context.getString(R.string.hide_creator_from_home),
"",
tag = "hide_creator",
call = {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
}))
+ actions).filterNotNull()
)); ));
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto", SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue", SlideUpMenuItem(container.context,
{ StatePlayer.instance.addToQueue(video); }), R.drawable.ic_queue_add,
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later", container.context.getString(R.string.add_to_queue),
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }) "${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem(container.context,
R.drawable.ic_history,
container.context.getString(R.string.add_to_history),
"Mark as watched",
tag = "history",
call = { StateHistory.instance.markAsWatched(video); }),
)); ));
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", { playlistItems.add(SlideUpMenuItem(
showCreatePlaylistOverlay(container) { container.context,
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video))); R.drawable.ic_playlist_add,
StatePlaylists.instance.createOrUpdatePlaylist(playlist); container.context.getString(R.string.new_playlist),
}; container.context.getString(R.string.add_to_new_playlist),
}, false)) tag = "add_to_new_playlist",
call = {
showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
};
},
invokeParent = false
))
for (playlist in allPlaylists) { for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "", playlistItems.add(SlideUpMenuItem(container.context,
{ R.drawable.ic_playlist_add,
StatePlaylists.instance.addToPlaylist(playlist.id, video); "${container.context.getString(R.string.add_to)} " + playlist.name + "",
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})); }));
} }
@ -695,9 +1046,14 @@ class UISlideOverlays {
if (lastUpdated != null) { if (lastUpdated != null) {
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist", SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "", SlideUpMenuItem(container.context,
{ R.drawable.ic_playlist_add,
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video); lastUpdated.name,
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})) }))
); );
@ -708,26 +1064,49 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other", SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue", SlideUpMenuItem(container.context,
{ StatePlayer.instance.addToQueue(video); }), R.drawable.ic_queue_add,
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later", container.context.getString(R.string.queue),
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }), "${queue.size} " + container.context.getString(R.string.videos),
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), tag = "queue",
{ showDownloadVideoOverlay(video, container, true); }, false)) call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false);
}),
)
); );
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", { playlistItems.add(SlideUpMenuItem(
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) { container.context,
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video))); R.drawable.ic_playlist_add,
StatePlaylists.instance.createOrUpdatePlaylist(playlist); container.context.getString(R.string.new_playlist),
}); container.context.getString(R.string.add_to_new_playlist),
}, false)) tag = "add_to_new_playlist",
call = {
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
});
},
invokeParent = false
))
for (playlist in allPlaylists) { for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "", playlistItems.add(SlideUpMenuItem(container.context,
{ R.drawable.ic_playlist_add,
StatePlaylists.instance.addToPlaylist(playlist.id, video); playlist.name,
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})); }));
} }
@ -738,8 +1117,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
} }
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters { fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues); val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
overlay.show(); overlay.show();
return overlay; return overlay;
} }
@ -751,40 +1130,74 @@ class UISlideOverlays {
val views = arrayOf( val views = arrayOf(
hidden hidden
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", { .map { btn -> SlideUpMenuItem(
btn.handler?.invoke(btn); container.context,
}, invokeParents) as View }.toTypedArray(), btn.iconResource,
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", { btn.text.text.toString(),
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) { "",
val selected = it tag = "",
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } } call = {
.filter { it != null } btn.handler?.invoke(btn);
.map { it!! } },
.toList(); invokeParent = invokeParents
) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(
container.context,
R.drawable.ic_pin,
container.context.getString(R.string.change_pins),
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
tag = "",
call = {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
val selected = it
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
.map { it!! }
.toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) }); onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
} });
}, false)) },
invokeParent = false
))
).flatten().toTypedArray(); ).flatten().toTypedArray();
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
} }
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
val selection: MutableList<Any> = mutableListOf(); val selection: MutableList<Any> = mutableListOf();
var overlay: SlideUpMenuOverlay? = null; var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true, overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, { listOf(
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
).filterNotNull() +
(options.map { SlideUpMenuItem(
container.context,
R.drawable.ic_move_up,
it.first,
"",
tag = it.second,
call = {
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
if(overlay!!.selectOption(null, it.second, true, true)) { if(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second)) if(!selection.contains(it.second)) {
selection.add(it.second); selection.add(it.second);
} if(overlayItem != null) {
else overlayItem.setSubText(selection.indexOf(it.second).toString());
}
}
} else {
selection.remove(it.second); selection.remove(it.second);
}, false) if(overlayItem != null) {
}); overlayItem.setSubText("");
}
}
},
invokeParent = false
)
}));
overlay.onOK.subscribe { overlay.onOK.subscribe {
onOrdered.invoke(selection); onOrdered.invoke(selection);
overlay.hide(); overlay.hide();

View file

@ -13,6 +13,7 @@ import android.os.OperationCanceledException
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.WindowInsetsController import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -25,12 +26,18 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.File import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.time.OffsetDateTime
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "; private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String { fun getRandomString(sizeOfRandomString: Int): String {
@ -229,4 +236,92 @@ fun String.decodeUnicode(): String {
i++ i++
} }
return sb.toString() return sb.toString()
}
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
val newArrResult = targetArr.toMutableList();
for(missing in missingToMerge) {
val newIndex = findNewIndex(toMerge, newArrResult, missing);
newArrResult.add(newIndex, missing);
}
return newArrResult;
}
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
var originalIndex = originalArr.indexOf(item);
var newIndex = -1;
for(i in originalIndex-1 downTo 0) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr + 1;
break;
}
}
if(newIndex < 0) {
for(i in originalIndex+1 until originalArr.size) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr - 1;
break;
}
}
}
if(newIndex < 0)
return originalArr.size;
else
return newIndex;
}
fun ByteBuffer.toUtf8String(): String {
val remainingBytes = ByteArray(remaining())
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
fun generateReadablePassword(length: Int): String {
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
val secureRandom = SecureRandom()
val randomBytes = ByteArray(length)
secureRandom.nextBytes(randomBytes)
val sb = StringBuilder(length)
for (byte in randomBytes) {
val index = (byte.toInt() and 0xFF) % validChars.length
sb.append(validChars[index])
}
return sb.toString()
}
fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val gzipTimeStart = OffsetDateTime.now();
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(this)
}
val result = outputStream.toByteArray();
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
return result;
}
fun ByteArray.fromGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val inputStream = ByteArrayInputStream(this)
val outputStream = ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
} }

View file

@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id }; val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) { StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
if(it) { if(it) {
StatePlatform.instance.clearUpdateAvailable(config) StatePlugins.instance.clearUpdateAvailable(config)
if(isNew) if(isNew)
lifecycleScope.launch { lifecycleScope.launch {
StatePlatform.instance.enableClient(listOf(config.id)); StatePlatform.instance.enableClient(listOf(config.id));

View file

@ -10,11 +10,13 @@ import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
class AddSourceOptionsActivity : AppCompatActivity() { class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton; lateinit var _buttonBack: ImageButton;
lateinit var _overlayContainer: FrameLayout;
lateinit var _buttonQR: BigButton; lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton; lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton; lateinit var _buttonURL: BigButton;
@ -54,6 +56,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
setContentView(R.layout.activity_add_source_options); setContentView(R.layout.activity_add_source_options);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
_overlayContainer = findViewById(R.id.overlay_container);
_buttonBack = findViewById(R.id.button_back); _buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr); _buttonQR = findViewById(R.id.option_qr);
@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
} }
_buttonURL.onClick.subscribe { _buttonURL.onClick.subscribe {
UIDialogs.toast(this, getString(R.string.not_implemented_yet)); val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
val content = nameInput.text;
val url = if (content.startsWith("https://")) {
content
} else if (content.startsWith("grayjay://plugin/")) {
content.substring("grayjay://plugin/".length)
} else {
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
return@showOverlay;
}
val intent = Intent(this, AddSourceActivity::class.java).apply {
data = Uri.parse(url);
};
startActivity(intent);
}, nameInput)
} }
} }
} }

View file

@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
companion object { companion object {
private val TAG = "LoginActivity"; private val TAG = "LoginActivity";
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*"); private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
private var _callback: ((SourceAuth?) -> Unit)? = null; private var _callback: ((SourceAuth?) -> Unit)? = null;

View file

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
@ -19,7 +20,12 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton 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.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix import com.google.zxing.common.BitMatrix
@ -64,11 +70,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
} }
_buttonShare.onClick.subscribe { _buttonShare.onClick.subscribe {
val shareIntent = Intent(Intent.ACTION_SEND).apply { val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
type = "text/plain"; startActivity(Intent.createChooser(shareIntent, "Share ID"));
putExtra(Intent.EXTRA_TEXT, _exportBundle);
}
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
}; };
_buttonCopy.onClick.subscribe { _buttonCopy.onClick.subscribe {

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
@ -10,13 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -25,6 +29,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton; private lateinit var _buttonHelp: ImageButton;
private lateinit var _profileName: EditText; private lateinit var _profileName: EditText;
private lateinit var _buttonCreate: LinearLayout; private lateinit var _buttonCreate: LinearLayout;
private lateinit var _loader: LoaderView;
private val TAG = "PolycentricCreateProfileActivity"; private val TAG = "PolycentricCreateProfileActivity";
private var _creating = false; private var _creating = false;
@ -41,6 +46,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help); _buttonHelp = findViewById(R.id.button_help);
_profileName = findViewById(R.id.edit_profile_name); _profileName = findViewById(R.id.edit_profile_name);
_buttonCreate = findViewById(R.id.button_create_profile); _buttonCreate = findViewById(R.id.button_create_profile);
_loader = findViewById(R.id.loader);
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
@ -63,29 +69,49 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
return@setOnClickListener; return@setOnClickListener;
} }
_profileName.isEnabled = false;
_buttonCreate.visibility = View.GONE;
_loader.start();
_loader.visibility = View.VISIBLE;
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val processHandle: ProcessHandle; val processHandle: ProcessHandle;
try { try {
processHandle = ProcessHandle.create(); try {
Store.instance.addProcessSecret(processHandle.processSecret); processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
processHandle.addServer("https://srv1-stg.polycentric.io"); try {
processHandle.setUsername(username); PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
StatePolycentric.instance.setProcessHandle(processHandle); } catch (e: Throwable) {
} catch (e: Throwable) { Logger.e(TAG, "Failed to save process secret to secret storage.", e)
Logger.e(TAG, getString(R.string.failed_to_create_profile), e); }
return@launch;
} finally { processHandle.addServer(ApiMethods.SERVER);
_creating = false; processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
try {
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
}
} }
finally {
try { withContext(Dispatchers.Main) {
Logger.i(TAG, "Started backfill"); _profileName.isEnabled = true;
processHandle.fullyBackfillServersAnnounceExceptions(); _buttonCreate.visibility = View.VISIBLE;
Logger.i(TAG, "Finished backfill"); _loader.stop();
} catch (e: Throwable) { _loader.visibility = View.GONE;
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e); }
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View file

@ -8,6 +8,7 @@ import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
@ -28,6 +29,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
private lateinit var _buttonNewProfile: BigButton; private lateinit var _buttonNewProfile: BigButton;
private lateinit var _buttonImportProfile: BigButton; private lateinit var _buttonImportProfile: BigButton;
private lateinit var _layoutButtons: LinearLayout; private lateinit var _layoutButtons: LinearLayout;
private lateinit var _scroll: ScrollView;
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -42,6 +44,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
_buttonNewProfile = findViewById(R.id.button_new_profile); _buttonNewProfile = findViewById(R.id.button_new_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile); _buttonImportProfile = findViewById(R.id.button_import_profile);
_layoutButtons = findViewById(R.id.layout_buttons); _layoutButtons = findViewById(R.id.layout_buttons);
_scroll = findViewById(R.id.scroll);
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
@ -78,6 +81,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
_layoutButtons.addView(profileButton, 0); _layoutButtons.addView(profileButton, 0);
} }
_scroll.invalidate();
_buttonHelp.setOnClickListener { _buttonHelp.setOnClickListener {
startActivity(Intent(this, PolycentricWhyActivity::class.java)); startActivity(Intent(this, PolycentricWhyActivity::class.java));

View file

@ -12,11 +12,12 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger 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.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret import com.futo.polycentric.core.ProcessSecret
@ -126,6 +127,12 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
val processSecret = ProcessSecret(keyPair, Process.random()); val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret); Store.instance.addProcessSecret(processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
val processHandle = processSecret.toProcessHandle(); val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) { for (e in exportBundle.events.eventsList) {
@ -138,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
} }
StatePolycentric.instance.setProcessHandle(processHandle); StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER); processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java)); startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish(); finish();

View file

@ -21,18 +21,20 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
@ -47,6 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton; private lateinit var _buttonHelp: ImageButton;
private lateinit var _editName: EditText; private lateinit var _editName: EditText;
private lateinit var _buttonExport: BigButton; private lateinit var _buttonExport: BigButton;
private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonLogout: BigButton; private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton; private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String; private lateinit var _username: String;
@ -68,10 +71,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric); _imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name); _editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export); _buttonExport = findViewById(R.id.button_export);
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonLogout = findViewById(R.id.button_logout); _buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete); _buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay); _loaderOverlay = findViewById(R.id.loader_overlay);
_textSystem = findViewById(R.id.text_system) _textSystem = findViewById(R.id.text_system)
findViewById<TextView>(R.id.text_cta2).setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://harbor.social")))
}
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired(); saveIfRequired();
finish(); finish();
@ -92,6 +99,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java)); startActivity(Intent(this, PolycentricBackupActivity::class.java));
}; };
_buttonOpenHarborProfile.onClick.subscribe {
val processHandle = StatePolycentric.instance.processHandle!!;
processHandle?.let {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
}
}
_buttonLogout.onClick.subscribe { _buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null); StatePolycentric.instance.setProcessHandle(null);
startActivity(Intent(this, PolycentricHomeActivity::class.java)); startActivity(Intent(this, PolycentricHomeActivity::class.java));
@ -108,6 +125,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
StatePolycentric.instance.setProcessHandle(null); StatePolycentric.instance.setProcessHandle(null);
Store.instance.removeProcessSecret(processHandle.system); Store.instance.removeProcessSecret(processHandle.system);
PolycentricStorage.instance.removeProcessSecret(processHandle.system);
startActivity(Intent(this, PolycentricHomeActivity::class.java)); startActivity(Intent(this, PolycentricHomeActivity::class.java));
finish(); finish();
}); });
@ -127,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER) processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateUI(); updateUI();

View file

@ -18,6 +18,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
@ -184,12 +185,19 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent); resultLauncher.launch(intent);
} }
override fun onDestroy() {
super.onDestroy()
settingsActivityClosed.emit()
}
companion object { companion object {
//TODO: Temporary for solving Settings issues //TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private var _lastActivity: SettingsActivity? = null; private var _lastActivity: SettingsActivity? = null;
val settingsActivityClosed = Event0()
fun getActivity(): SettingsActivity? { fun getActivity(): SettingsActivity? {
val act = _lastActivity; val act = _lastActivity;
if(act != null && !act._isFinished) if(act != null && !act._isFinished)

View file

@ -0,0 +1,140 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.LinkType
import com.futo.platformplayer.sync.internal.SyncSession
import com.futo.platformplayer.views.sync.SyncDeviceView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SyncHomeActivity : AppCompatActivity() {
private lateinit var _layoutDevices: LinearLayout
private lateinit var _layoutEmpty: LinearLayout
private val _viewMap: MutableMap<String, SyncDeviceView> = mutableMapOf()
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons()
_layoutDevices = findViewById(R.id.layout_devices)
_layoutEmpty = findViewById(R.id.layout_empty)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncPairActivity::class.java))
}
findViewById<LinearLayout>(R.id.button_show_pairing_code).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncShowPairingCodeActivity::class.java))
}
initializeDevices()
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { publicKey, session ->
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[publicKey]
if (!session.isAuthorized) {
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey)
}
return@launch
}
if (view == null) {
val syncDeviceView = SyncDeviceView(this@SyncHomeActivity)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val v = updateDeviceView(syncDeviceView, publicKey, session)
_layoutDevices.addView(v, 0)
_viewMap[publicKey] = v
} else {
updateDeviceView(view, publicKey, session)
}
updateEmptyVisibility()
}
}
StateSync.instance.deviceRemoved.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[it]
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(it)
}
updateEmptyVisibility()
}
}
}
override fun onDestroy() {
super.onDestroy()
StateSync.instance.deviceUpdatedOrAdded.remove(this)
StateSync.instance.deviceRemoved.remove(this)
}
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView
}
private fun updateEmptyVisibility() {
if (_viewMap.isNotEmpty()) {
_layoutEmpty.visibility = View.GONE
} else {
_layoutEmpty.visibility = View.VISIBLE
}
}
private fun initializeDevices() {
_layoutDevices.removeAllViews()
for (publicKey in StateSync.instance.getAll()) {
val syncDeviceView = SyncDeviceView(this)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val view = updateDeviceView(syncDeviceView, publicKey, StateSync.instance.getSession(publicKey))
_layoutDevices.addView(view)
_viewMap[publicKey] = view
}
updateEmptyVisibility()
}
companion object {
private const val TAG = "SyncHomeActivity"
}
}

View file

@ -0,0 +1,148 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.util.Base64
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
class SyncPairActivity : AppCompatActivity() {
private lateinit var _editCode: EditText
private lateinit var _layoutPairing: LinearLayout
private lateinit var _textPairingStatus: TextView
private lateinit var _layoutPairingSuccess: LinearLayout
private lateinit var _layoutPairingError: LinearLayout
private lateinit var _textError: TextView
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
scanResult?.let {
if (it.contents != null) {
_editCode.text.clear()
_editCode.text.append(it.contents)
pair(it.contents)
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_pair)
setNavigationBarColorAndIcons()
_editCode = findViewById(R.id.edit_code)
_layoutPairing = findViewById(R.id.layout_pairing)
_textPairingStatus = findViewById(R.id.text_pairing_status)
_layoutPairingSuccess = findViewById(R.id.layout_pairing_success)
_layoutPairingError = findViewById(R.id.layout_pairing_error)
_textError = findViewById(R.id.text_error)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_scan_qr).setOnClickListener {
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true);
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
integrator.setBarcodeImageEnabled(true)
integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent())
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
pair(_editCode.text.toString())
}
_layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE
}
_layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE
}
_layoutPairingSuccess.visibility = View.GONE
_layoutPairingError.visibility = View.GONE
}
fun pair(url: String) {
try {
_layoutPairing.visibility = View.VISIBLE
_textPairingStatus.text = "Parsing text..."
if (!url.startsWith("grayjay://sync/")) {
throw Exception("Not a valid URL: $url")
}
val deviceInfo: SyncDeviceInfo = Json.decodeFromString<SyncDeviceInfo>(Base64.decode(url.substring("grayjay://sync/".length), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).decodeToString())
if (StateSync.instance.isAuthorized(deviceInfo.publicKey)) {
throw Exception("This device is already paired")
}
_textPairingStatus.text = "Connecting..."
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.connect(deviceInfo) { complete, message ->
lifecycleScope.launch(Dispatchers.Main) {
if (complete != null && complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textPairingStatus.text = message
}
}
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
_layoutPairingError.visibility = View.VISIBLE
if(e.message == "Failed to connect") {
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
}
else
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
}
}
} catch(e: Throwable) {
_layoutPairingError.visibility = View.VISIBLE
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
}
}
companion object {
private const val TAG = "SyncPairActivity"
}
}

View file

@ -0,0 +1,142 @@
package com.futo.platformplayer.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.NetworkInterface
class SyncShowPairingCodeActivity : AppCompatActivity() {
private lateinit var _textCode: TextView
private lateinit var _imageQR: ImageView
private lateinit var _textQR: TextView
private var _code: String? = null
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onDestroy() {
super.onDestroy()
activity = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity = this
setContentView(R.layout.activity_sync_show_pairing_code)
setNavigationBarColorAndIcons()
_textCode = findViewById(R.id.text_code)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_scan_qr)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_copy).setOnClickListener {
val code = _code ?: return@setOnClickListener
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
val clip = ClipData.newPlainText(getString(R.string.copied_text), code);
clipboard.setPrimaryClip(clip);
UIDialogs.toast(this, "Copied to clipboard")
}
val ips = getIPs()
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.pairingCode)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
fun setCode(code: String?) {
_code = code
_textCode.text = code
if (code == null) {
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
return
}
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
_imageQR.setImageBitmap(qrCodeBitmap)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
}
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
return bitMatrixToBitmap(bitMatrix);
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
val width = matrix.width;
val height = matrix.height;
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
}
}
return bmp;
}
private fun getIPs(): List<String> {
val ips = arrayListOf<String>()
for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) {
if (addr.isLoopbackAddress) {
continue
}
if (addr.address.size != 4) {
continue
}
addr.hostAddress?.let { ips.add(it) }
}
}
return ips
}
companion object {
private const val TAG = "SyncShowPairingCodeActivity"
var activity: SyncShowPairingCodeActivity? = null
private set
}
}

View file

@ -1,8 +1,12 @@
package com.futo.platformplayer.api.http 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.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.FragmentedStorage
import okhttp3.Call import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -13,10 +17,16 @@ import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
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 import kotlin.system.measureTimeMillis
open class ManagedHttpClient { open class ManagedHttpClient {
protected val _builderTemplate: OkHttpClient.Builder; protected var _builderTemplate: OkHttpClient.Builder;
private var client: OkHttpClient; private var client: OkHttpClient;
@ -25,8 +35,38 @@ open class ManagedHttpClient {
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0" 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()) { constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder; _builderTemplate = builder;
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain -> client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request()); val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request)); val response = afterRequest(chain.proceed(request));
@ -34,6 +74,15 @@ open class ManagedHttpClient {
}.build(); }.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 { open fun clone(): ManagedHttpClient {
val clonedClient = ManagedHttpClient(_builderTemplate); val clonedClient = ManagedHttpClient(_builderTemplate);
clonedClient.user_agent = user_agent; clonedClient.user_agent = user_agent;

View file

@ -210,6 +210,20 @@ class HttpContext : AutoCloseable {
} }
} }
} }
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) { fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set"); val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");

View file

@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException 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 com.futo.platformplayer.api.http.server.handlers.HttpHandler
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for(getMethod in getMethods) for(getMethod in getMethods)
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1) 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()) if(!getMethod.second.contentType.isEmpty())
this.withContentType(getMethod.second.contentType); this.withContentType(getMethod.second.contentType);
}.withContentType(getMethod.second.contentType); }.withContentType(getMethod.second.contentType);
for(postMethod in postMethods) for(postMethod in postMethods)
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1) 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()) if(!postMethod.second.contentType.isEmpty())
this.withContentType(postMethod.second.contentType); this.withContentType(postMethod.second.contentType);
}.withContentType(postMethod.second.contentType); }.withContentType(postMethod.second.contentType);
for(getField in getFields) { for(getField in getFields) {
getField.first.isAccessible = true; getField.first.isAccessible = true;
addHandler(HttpFuntionHandler("GET", getField.second.path) { addHandler(HttpFunctionHandler("GET", getField.second.path) {
val value = getField.first.get(obj) as String?; val value = getField.first.get(obj) as String?;
if(value != null) { if(value != null) {
val headers = HttpHeaders( val headers = HttpHeaders(

View file

@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent") Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
current += bytesToSend.toLong() current += bytesToSend.toLong()
if (current >= end) { if (current > end) {
Logger.i(TAG, "Expected amount of bytes sent") Logger.i(TAG, "Expected amount of bytes sent")
break break
} }

View file

@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext 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) { override fun handle(httpContext: HttpContext) {
httpContext.setResponseHeaders(this.headers); httpContext.setResponseHeaders(this.headers);
handler(httpContext); handler(httpContext);

View file

@ -1,103 +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.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
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.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
/**
* 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);
_cache.put(url, result);
}
return result;
}
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
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);
}
}

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.api.media package com.futo.platformplayer.api.media
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -10,6 +11,7 @@ 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.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent 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.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.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
@ -65,6 +67,11 @@ interface IPlatformClient {
*/ */
fun searchChannels(query: String): IPager<PlatformAuthorLink>; fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages //Video Pages
/** /**
@ -84,6 +91,20 @@ interface IPlatformClient {
*/ */
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>; 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 * Gets the channel url associated with a claimType
*/ */
@ -106,6 +127,11 @@ interface IPlatformClient {
*/ */
fun getPlaybackTracker(url: String): IPlaybackTracker?; fun getPlaybackTracker(url: String): IPlaybackTracker?;
/**
* Get content recommendations
*/
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
//Comments //Comments
/** /**

View file

@ -13,10 +13,14 @@ data class PlatformClientCapabilities(
val hasGetChannelUrlByClaim: Boolean = false, val hasGetChannelUrlByClaim: Boolean = false,
val hasGetChannelTemplateByClaimMap: Boolean = false, val hasGetChannelTemplateByClaimMap: Boolean = false,
val hasGetSearchCapabilities: Boolean = false, val hasGetSearchCapabilities: Boolean = false,
val hasGetSearchChannelContentsCapabilities: Boolean = false,
val hasGetChannelCapabilities: Boolean = false, val hasGetChannelCapabilities: Boolean = false,
val hasGetLiveEvents: Boolean = false, val hasGetLiveEvents: Boolean = false,
val hasGetLiveChatWindow: Boolean = false, val hasGetLiveChatWindow: Boolean = false,
val hasGetContentChapters: Boolean = false val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false
) { ) {
} }

View file

@ -13,13 +13,15 @@ class PlatformClientPool {
private val _pool: HashMap<JSClient, Int> = hashMapOf(); private val _pool: HashMap<JSClient, Int> = hashMapOf();
private var _poolCounter = 0; private var _poolCounter = 0;
private val _poolName: String?; private val _poolName: String?;
private val _privatePool: Boolean;
var isDead: Boolean = false var isDead: Boolean = false
private set; private set;
val onDead = Event2<JSClient, PlatformClientPool>(); val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null) { constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
_poolName = name; _poolName = name;
_privatePool = privatePool;
if(parentClient !is JSClient) if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now"); throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started"); Logger.i(TAG, "Pool for ${parentClient.name} was started");
@ -51,7 +53,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy }; reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) { if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${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 -> reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex); StateApp.instance.handleCaptchaException(client, ex);

View file

@ -6,12 +6,14 @@ class PlatformMultiClientPool {
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf(); private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
private var _isFake = false; 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; _name = name;
_maxCap = if(maxCap > 0) _maxCap = if(maxCap > 0)
maxCap maxCap
else 99; else 99;
_privatePool = isPrivatePool;
} }
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient { fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@ -19,7 +21,7 @@ class PlatformMultiClientPool {
return parentClient; return parentClient;
val pool = synchronized(_clientPools) { val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient)) if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply { _clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
this.onDead.subscribe { _, pool -> this.onDead.subscribe { _, pool ->
synchronized(_clientPools) { synchronized(_clientPools) {
if(_clientPools[parentClient] == pool) if(_clientPools[parentClient] == pool)

View file

@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
class Serializer { class Serializer {
companion object { companion object {
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; }; val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
} }
} }

View file

@ -2,7 +2,10 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -14,7 +17,7 @@ open class PlatformAuthorLink {
val id: PlatformID; val id: PlatformID;
val name: String; val name: String;
val url: String; val url: String;
val thumbnail: String?; var thumbnail: String?;
var subscribers: Long? = null; //Optional var subscribers: Long? = null; //Optional
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null) constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
@ -27,6 +30,8 @@ open class PlatformAuthorLink {
} }
companion object { companion object {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
if(value.has("membershipUrl")) if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value); return PlatformAuthorMembershipLink.fromV8(config, value);
@ -40,4 +45,21 @@ open class PlatformAuthorLink {
); );
} }
} }
}
interface IPlatformChannelContent : IPlatformContent {
val thumbnail: String?
val subscribers: Long?
}
open class JSChannelContent : JSContent, IPlatformChannelContent {
override val contentType: ContentType get() = ContentType.CHANNEL
override val thumbnail: String?
override val subscribers: Long?
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Channel";
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
}
} }

View file

@ -30,6 +30,7 @@ class ResultCapabilities(
const val TYPE_POSTS = "POSTS"; const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED"; const val TYPE_MIXED = "MIXED";
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS"; const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
const val TYPE_SHORTS = "SHORTS";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL"; const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";

View file

@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@ -31,7 +33,7 @@ class Thumbnails {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails")) return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray() .toArray()
.map { Thumbnail.fromV8(it as V8ValueObject) } .map { Thumbnail.fromV8(config, it as V8ValueObject) }
.toTypedArray()); .toTypedArray());
} }
} }
@ -40,10 +42,10 @@ class Thumbnails {
data class Thumbnail(val url : String?, val quality : Int = 0) { data class Thumbnail(val url : String?, val quality : Int = 0) {
companion object { companion object {
fun fromV8(value: V8ValueObject): Thumbnail { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
return Thumbnail( return Thumbnail(
value.getString("url"), value.getOrDefault<String>(config,"url", "Thumbnail", null),
value.getInteger("quality")); value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
} }
} }
}; };

View file

@ -37,6 +37,10 @@ class SerializedChannel(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object { companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel { fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel( return SerializedChannel(

View file

@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
companion object { companion object {
fun fromInt(value: Int): ChapterType fun fromInt(value: Int): ChapterType
{ {
val result = ChapterType.values().firstOrNull { it.value == value }; val result = ChapterType.entries.firstOrNull { it.value == value };
if(result == null) if(result == null)
throw UnknownPlatformException(value.toString()); throw UnknownPlatformException(value.toString());
return result; return result;

View file

@ -0,0 +1,63 @@
package com.futo.platformplayer.api.media.models.comments
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.ratings.RatingType
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Deferred
import java.time.OffsetDateTime
class LazyComment: IPlatformComment {
private var _commentDeferred: Deferred<IPlatformComment>;
private var _commentLoaded: IPlatformComment? = null;
private var _commentException: Throwable? = null;
override val contextUrl: String
get() = _commentLoaded?.contextUrl ?: "";
override val author: PlatformAuthorLink
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
override val message: String
get() = _commentLoaded?.message ?: "";
override val rating: IRating
get() = _commentLoaded?.rating ?: RatingLikes(0);
override val date: OffsetDateTime?
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
override val replyCount: Int?
get() = _commentLoaded?.replyCount ?: 0;
val isAvailable: Boolean get() = _commentLoaded != null;
private var _uiHandler: ((LazyComment)->Unit)? = null;
constructor(commentDeferred: Deferred<IPlatformComment>) {
_commentDeferred = commentDeferred;
_commentDeferred.invokeOnCompletion {
if(it == null) {
_commentLoaded = commentDeferred.getCompleted();
Logger.i("LazyComment", "Resolved comment");
}
else {
_commentException = it;
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
}
_uiHandler?.invoke(this);
}
}
fun getUnderlyingComment(): IPlatformComment? {
return _commentLoaded;
}
fun setUIHandler(handler: (LazyComment)->Unit){
_uiHandler = handler;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
return _commentLoaded?.getReplies(client);
}
}

View file

@ -12,6 +12,7 @@ enum class ContentType(val value: Int) {
URL(9), URL(9),
NESTED_VIDEO(11), NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70), LOCKED(70),
@ -21,7 +22,7 @@ enum class ContentType(val value: Int) {
companion object { companion object {
fun fromInt(value: Int): ContentType fun fromInt(value: Int): ContentType
{ {
val result = ContentType.values().firstOrNull { it.value == value }; val result = ContentType.entries.firstOrNull { it.value == value };
if(result == null) if(result == null)
throw UnknownPlatformException(value.toString()); throw UnknownPlatformException(value.toString());
return result; return result;

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.contents
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime import java.time.OffsetDateTime
interface IPlatformContent { interface IPlatformContent {

View file

@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?; fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
fun getPlaybackTracker(): IPlaybackTracker?; fun getPlaybackTracker(): IPlaybackTracker?;
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
} }

View file

@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.live
interface ILiveChatWindowDescriptor { interface ILiveChatWindowDescriptor {
val url: String; val url: String;
val removeElements: List<String>; val removeElements: List<String>;
val removeElementsInterval: List<String>;
} }

View file

@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
companion object{ companion object{
fun fromInt(value : Int) : LiveEventType{ fun fromInt(value : Int) : LiveEventType{
return LiveEventType.values().first { it.value == value }; return LiveEventType.entries.first { it.value == value };
} }
} }
} }

View file

@ -7,4 +7,5 @@ interface IPlaybackTracker {
fun onInit(seconds: Double); fun onInit(seconds: Double);
fun onProgress(seconds: Double, isPlaying: Boolean); fun onProgress(seconds: Double, isPlaying: Boolean);
fun onConcluded();
} }

View file

@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
//TODO: Determine if this should be IPlatformContent (probably not?) //TODO: Determine if this should be IPlatformContent (probably not?)
val contents: IPager<IPlatformVideo>; val contents: IPager<IPlatformVideo>;
fun toPlaylist(): Playlist; fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
} }

View file

@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
companion object { companion object {
fun fromInt(value: Int): TextType fun fromInt(value: Int): TextType
{ {
val result = TextType.values().firstOrNull { it.value == value }; val result = TextType.entries.firstOrNull { it.value == value };
if(result == null) if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value"); throw IllegalArgumentException("Unknown Texttype: $value");
return result; return result;

View file

@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
companion object{ companion object{
fun fromInt(value : Int) : RatingType{ fun fromInt(value : Int) : RatingType{
return RatingType.values().first { it.value == value }; return RatingType.entries.first { it.value == value };
} }
} }
} }

View file

@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoMuxedSourceDescriptor( class DownloadedVideoMuxedSourceDescriptor(
private val video: VideoLocal private val video: VideoLocal
) : VideoMuxedSourceDescriptor() { ) : VideoMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray(); override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();

View file

@ -13,7 +13,8 @@ class AudioUrlSource(
override val codec: String = "", override val codec: String = "",
override val language: String = Language.UNKNOWN, override val language: String = Language.UNKNOWN,
override val duration: Long? = null, override val duration: Long? = null,
override var priority: Boolean = false override var priority: Boolean = false,
override var original: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{ ) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null; override var streamMetaData: StreamMetaData? = null;
@ -36,7 +37,9 @@ class AudioUrlSource(
source.container, source.container,
source.codec, source.codec,
source.language, source.language,
source.duration source.duration,
source.priority,
source.original
); );
ret.streamMetaData = streamData; ret.streamMetaData = streamData;

View file

@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
override val language: String, override val language: String,
override val duration: Long?, override val duration: Long?,
override val priority: Boolean, override val priority: Boolean,
override val original: Boolean,
val url: String val url: String
) : IAudioUrlSource { ) : IAudioUrlSource {
override fun getAudioUrl(): String { override fun getAudioUrl(): String {

View file

@ -8,4 +8,5 @@ interface IAudioSource {
val language : String; val language : String;
val duration : Long?; val duration : Long?;
val priority: Boolean; val priority: Boolean;
val original: Boolean;
} }

View file

@ -0,0 +1,3 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource

View file

@ -0,0 +1,5 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IDashManifestWidevineSource : IWidevineSource {
val url: String
}

View file

@ -0,0 +1,3 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource

View file

@ -0,0 +1,9 @@
package com.futo.platformplayer.api.media.models.streams.sources
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
interface IWidevineSource {
val licenseUri: String
val hasLicenseRequestExecutor: Boolean
fun getLicenseRequestExecutor(): JSRequestExecutor?
}

View file

@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
override val duration: Long? = null; override val duration: Long? = null;
override var priority: Boolean = false; override var priority: Boolean = false;
override val original: Boolean = false;
val filePath : String; val filePath : String;
val fileSize: Long; val fileSize: Long;
@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
} }
companion object { companion object {
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource { fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
return LocalAudioSource( return LocalAudioSource(
source.name, source.name,
path, path,
fileSize, fileSize,
source.bitrate, source.bitrate,
source.container, overrideContainer ?: source.container,
source.codec, source.codec,
source.language source.language
); );

View file

@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
} }
companion object { companion object {
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource { fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
return LocalVideoSource( return LocalVideoSource(
source.name, source.name,
path, path,
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
source.width, source.width,
source.height, source.height,
source.duration, source.duration,
source.container, overrideContainer ?: source.container,
source.codec, source.codec,
source.bitrate?:0 source.bitrate?:0
); );

View file

@ -13,4 +13,6 @@ interface IPlatformVideo : IPlatformContent {
val viewCount: Long; val viewCount: Long;
val isLive : Boolean; val isLive : Boolean;
val isShort: Boolean;
} }

View file

@ -10,23 +10,26 @@ import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime import java.time.OffsetDateTime
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
open class SerializedPlatformVideo( open class SerializedPlatformVideo(
override val contentType: ContentType = ContentType.MEDIA,
override val id: PlatformID, override val id: PlatformID,
override val name: String, override val name: String,
override val thumbnails: Thumbnails, override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink, override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override val datetime: OffsetDateTime?, @JsonNames("datetime", "dateTime")
override val datetime: OffsetDateTime? = null,
override val url: String, override val url: String,
override val shareUrl: String, override val shareUrl: String = "",
override val duration: Long, override val duration: Long,
override val viewCount: Long, override val viewCount: Long,
override val isShort: Boolean = false
) : IPlatformVideo, SerializedPlatformContent { ) : IPlatformVideo, SerializedPlatformContent {
override val contentType: ContentType = ContentType.MEDIA;
override val isLive: Boolean = false; override val isLive: Boolean = false;
@ -43,6 +46,7 @@ open class SerializedPlatformVideo(
companion object { companion object {
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo { fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
return SerializedPlatformVideo( return SerializedPlatformVideo(
ContentType.MEDIA,
video.id, video.id,
video.name, video.name,
video.thumbnails, video.thumbnails,

View file

@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.sources.* import com.futo.platformplayer.api.media.models.streams.sources.*
@ -37,7 +38,8 @@ open class SerializedPlatformVideoDetails(
override val video: ISerializedVideoSourceDescriptor, override val video: ISerializedVideoSourceDescriptor,
override val preview: ISerializedVideoSourceDescriptor?, override val preview: ISerializedVideoSourceDescriptor?,
override val subtitles: List<SubtitleRawSource> = listOf() override val subtitles: List<SubtitleRawSource> = listOf(),
override val isShort: Boolean = false
) : IPlatformVideo, IPlatformVideoDetails { ) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA; final override val contentType: ContentType get() = ContentType.MEDIA;
@ -56,6 +58,7 @@ open class SerializedPlatformVideoDetails(
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null; override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null; override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
companion object { companion object {
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails { fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {

View file

@ -8,6 +8,8 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
class DevJSClient : JSClient { class DevJSClient : JSClient {
@ -52,8 +54,8 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings); return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
} }
override fun getCopy(): JSClient { override fun getCopy(privateCopy: Boolean): JSClient {
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID); return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
} }
override fun initialize() { override fun initialize() {
@ -115,7 +117,7 @@ class DevJSClient : JSClient {
//Video //Video
override fun isContentDetailsUrl(url: String): Boolean { override fun isContentDetailsUrl(url: String): Boolean {
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl"){ return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl(${Json.encodeToString(url)})"){
super.isContentDetailsUrl(url); super.isContentDetailsUrl(url);
}; };
} }

View file

@ -10,6 +10,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -20,6 +21,7 @@ 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.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent 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.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.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
@ -27,8 +29,10 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment import com.futo.platformplayer.api.media.platforms.js.models.JSComment
@ -38,6 +42,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveChatWindowDes
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@ -45,6 +50,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginEngineException import com.futo.platformplayer.engine.exceptions.PluginEngineException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptValidationException import com.futo.platformplayer.engine.exceptions.ScriptValidationException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -56,8 +62,10 @@ import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import kotlin.Exception
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
import kotlin.streams.asSequence
open class JSClient : IPlatformClient { open class JSClient : IPlatformClient {
val config: SourcePluginConfig; val config: SourcePluginConfig;
@ -73,6 +81,7 @@ open class JSClient : IPlatformClient {
private var _searchCapabilities: ResultCapabilities? = null; private var _searchCapabilities: ResultCapabilities? = null;
private var _searchChannelContentsCapabilities: ResultCapabilities? = null; private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
private var _channelCapabilities: ResultCapabilities? = null; private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null;
protected val _script: String; protected val _script: String;
@ -91,7 +100,11 @@ open class JSClient : IPlatformClient {
private val _busyLock = Object(); private val _busyLock = Object();
private var _busyCounter = 0; private var _busyCounter = 0;
private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0; val isBusy: Boolean get() = _busyCounter > 0;
val isBusyAction: String get() {
return _busyAction;
}
val settings: HashMap<String, String?> get() = descriptor.settings; val settings: HashMap<String, String?> get() = descriptor.settings;
@ -150,14 +163,19 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException) if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it); onCaptchaException.emit(this, it);
}; };
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
this._context = context; this._context = context;
this.config = descriptor.config; this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null); icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor; this.descriptor = descriptor;
_injectedSaveState = saveState; _injectedSaveState = saveState;
_auth = descriptor.getAuth(); if(!withoutCredentials)
_auth = descriptor.getAuth();
else
_auth = null;
_captcha = descriptor.getCaptchaData(); _captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
@ -173,10 +191,12 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException) if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it); onCaptchaException.emit(this, it);
}; };
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
open fun getCopy(): JSClient { open fun getCopy(withoutCredentials: Boolean = false): JSClient {
return JSClient(_context, descriptor, saveState(), _script); return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
} }
fun getUnderlyingPlugin(): V8Plugin { fun getUnderlyingPlugin(): V8Plugin {
@ -214,9 +234,13 @@ open class JSClient : IPlatformClient {
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false, hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false, hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false, hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false, hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false, hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
); );
try { try {
@ -260,7 +284,7 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform") @JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
override fun getHome(): IPager<IPlatformContent> = isBusyWith { override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSContentPager(config, this, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getHome()")); plugin.executeTyped("source.getHome()"));
@ -268,7 +292,7 @@ open class JSClient : IPlatformClient {
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query") @JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for") @JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith { override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
ensureEnabled(); ensureEnabled();
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})") return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
.toArray() .toArray()
@ -298,7 +322,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
@JSDocsParameter("channelId", "(optional) Channel id to search in") @JSDocsParameter("channelId", "(optional) Channel id to search in")
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSContentPager(config, this, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
@ -306,6 +330,9 @@ open class JSClient : IPlatformClient {
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos") @JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
override fun getSearchChannelContentsCapabilities(): ResultCapabilities { override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
if(!capabilities.hasGetSearchChannelContentsCapabilities)
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
ensureEnabled(); ensureEnabled();
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;
@ -319,7 +346,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("type", "(optional) Type of contents to get from search ") @JSDocsParameter("type", "(optional) Type of contents to get from search ")
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSearchChannelContents) if(!capabilities.hasSearchChannelContents)
throw IllegalStateException("This plugin does not support channel search"); throw IllegalStateException("This plugin does not support channel search");
@ -331,11 +358,15 @@ open class JSClient : IPlatformClient {
@JSOptional @JSOptional
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform") @JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
@JSDocsParameter("query", "Query that channels should match") @JSDocsParameter("query", "Query that channels should match")
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith { override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSChannelPager(config, this, return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
} }
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
ensureEnabled();
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
}
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform") @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)") @JSDocsParameter("url", "A channel url (May not be your platform)")
@ -351,7 +382,7 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@JSDocsParameter("channelUrl", "A channel url (this platform)") @JSDocsParameter("channelUrl", "A channel url (this platform)")
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith { override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSChannel(config, return@isBusyWith JSChannel(config,
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})")); plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
@ -378,12 +409,57 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("type", "(optional) Type of contents to get from channel") @JSDocsParameter("type", "(optional) Type of contents to get from channel")
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSContentPager(config, this, return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
} }
@JSDocs(10, "source.getChannelPlaylists(url)", "Gets playlists of a channel")
@JSDocsParameter("channelUrl", "A channel url (this platform)")
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = isBusyWith("getChannelPlaylists") {
ensureEnabled();
if(!capabilities.hasGetChannelPlaylists)
return@isBusyWith EmptyPager();
return@isBusyWith JSPlaylistPager(config, this,
plugin.executeTyped("source.getChannelPlaylists(${Json.encodeToString(channelUrl)})"));
}
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
override fun getPeekChannelTypes(): List<String> {
if(!capabilities.hasPeekChannelContents)
return listOf();
try {
if (_peekChannelTypes != null) {
return _peekChannelTypes!!;
}
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
_peekChannelTypes = arr.keys.mapNotNull {
val str = arr.get<V8ValueString>(it);
return@mapNotNull str.value;
};
return _peekChannelTypes ?: listOf();
}
catch(ex: Throwable) {
announcePluginUnhandledException("getPeekChannelTypes", ex);
return listOf();
}
}
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
@JSDocsParameter("channelUrl", "A channel url (this platform)")
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
ensureEnabled();
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
return@isBusyWith items.keys.mapNotNull {
val obj = items.get<V8ValueObject>(it);
return@mapNotNull IJSContent.fromV8(this, obj);
};
}
@JSOptional @JSOptional
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim") @JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
@JSDocsParameter("claimType", "Polycentric claimtype id") @JSDocsParameter("claimType", "Polycentric claimtype id")
@ -444,7 +520,7 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith { override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
ensureEnabled(); ensureEnabled();
return@isBusyWith IJSContentDetails.fromV8(this, return@isBusyWith IJSContentDetails.fromV8(this,
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})")); plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
@ -453,7 +529,7 @@ open class JSClient : IPlatformClient {
@JSOptional //getContentChapters = function(url, initialData) @JSOptional //getContentChapters = function(url, initialData)
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details") @JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getContentChapters(url: String): List<IChapter> = isBusyWith { override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
if(!capabilities.hasGetContentChapters) if(!capabilities.hasGetContentChapters)
return@isBusyWith listOf(); return@isBusyWith listOf();
ensureEnabled(); ensureEnabled();
@ -464,7 +540,7 @@ open class JSClient : IPlatformClient {
@JSOptional @JSOptional
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url") @JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith { override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
if(!capabilities.hasGetPlaybackTracker) if(!capabilities.hasGetPlaybackTracker)
return@isBusyWith null; return@isBusyWith null;
ensureEnabled(); ensureEnabled();
@ -478,7 +554,7 @@ open class JSClient : IPlatformClient {
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url") @JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
@JSDocsParameter("url", "A content url (this platform)") @JSDocsParameter("url", "A content url (this platform)")
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith { override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
ensureEnabled(); ensureEnabled();
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})"); val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
if (pager !is V8ValueObject) { //TODO: Maybe solve this better if (pager !is V8ValueObject) { //TODO: Maybe solve this better
@ -494,31 +570,45 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})")); plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
} }
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream") @JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream") @JSDocsParameter("url", "Url of live stream")
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith { override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
if(!capabilities.hasGetLiveChatWindow) if(!capabilities.hasGetLiveChatWindow)
return@isBusyWith null; return@isBusyWith null;
ensureEnabled(); ensureEnabled();
return@isBusyWith JSLiveChatWindowDescriptor(config, return@isBusyWith JSLiveChatWindowDescriptor(config,
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})")); plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
} }
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream") @JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream") @JSDocsParameter("url", "Url of live stream")
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith { override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
if(!capabilities.hasGetLiveEvents) if(!capabilities.hasGetLiveEvents)
return@isBusyWith null; return@isBusyWith null;
ensureEnabled(); ensureEnabled();
return@isBusyWith JSLiveEventPager(config, this, return@isBusyWith JSLiveEventPager(config, this,
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
} }
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
if(!capabilities.hasGetContentRecommendations)
return@isBusyWith null;
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform") @JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
@JSDocsParameter("query", "Query that search results should match") @JSDocsParameter("query", "Query that search results should match")
@JSDocsParameter("type", "(optional) Type of contents to get from search ") @JSDocsParameter("type", "(optional) Type of contents to get from search ")
@JSDocsParameter("order", "(optional) Order in which contents should be returned") @JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents") @JSDocsParameter("filters", "(optional) Filters to apply on contents")
@JSDocsParameter("channelId", "(optional) Channel id to search in") @JSDocsParameter("channelId", "(optional) Channel id to search in")
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSearchPlaylists) if(!capabilities.hasSearchPlaylists)
throw IllegalStateException("This plugin does not support playlist search"); throw IllegalStateException("This plugin does not support playlist search");
@ -528,15 +618,22 @@ open class JSClient : IPlatformClient {
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean { override fun isPlaylistUrl(url: String): Boolean {
ensureEnabled();
if (!capabilities.hasGetPlaylist) if (!capabilities.hasGetPlaylist)
return false; return false;
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return false;
}
} }
@JSOptional @JSOptional
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user") @JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith { override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
ensureEnabled(); ensureEnabled();
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})")); return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
} }
@ -633,19 +730,24 @@ open class JSClient : IPlatformClient {
} }
private fun <T> isBusyWith(handle: ()->T): T { private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try { try {
synchronized(_busyLock) { synchronized(_busyLock) {
_busyCounter++; _busyCounter++;
} }
_busyAction = actionName;
return handle(); return handle();
} }
finally { finally {
_busyAction = "";
synchronized(_busyLock) { synchronized(_busyLock) {
_busyCounter--; _busyCounter--;
} }
} }
} }
private fun <T> isBusyWith(handle: ()->T): T {
return isBusyWith("Unknown", handle);
}
private fun announcePluginUnhandledException(method: String, ex: Throwable) { private fun announcePluginUnhandledException(method: String, ex: Throwable) {
if(ex is PluginEngineException) if(ex is PluginEngineException)
@ -662,10 +764,43 @@ open class JSClient : IPlatformClient {
companion object { companion object {
val TAG = "JSClient"; val TAG = "JSClient";
private val _lock = Object();
private var _docs: Map<String, String>? = null;
fun getMethodDocs(names: List<String>): Map<String, String>? {
synchronized(_lock) {
if(_docs == null) {
val client = ManagedHttpClient();
val docs = names
.map { stringWithoutBrackets(it) }
.distinct()
.parallelStream()
.map {
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
val resp = client.head(url);
if(resp.isOk)
return@map Pair(it, url);
else
return@map null;
}.asSequence()
.filterNotNull()
.toMap();
_docs = docs;
}
return _docs;
}
}
fun getMethodDocUrls(): Map<String, String>? {
if(_docs != null)
return _docs;
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
return getMethodDocs(methods.map { it.name });
}
fun getJSDocs(): List<JSCallDocs> { fun getJSDocs(): List<JSCallDocs> {
val docs = mutableListOf<JSCallDocs>(); val docs = mutableListOf<JSCallDocs>();
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null } val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) { for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
val doc = method.getAnnotation(JSDocs::class.java); val doc = method.getAnnotation(JSDocs::class.java);
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>(); val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
@ -678,5 +813,12 @@ open class JSClient : IPlatformClient {
} }
return docs; return docs;
} }
private fun stringWithoutBrackets(name: String): String {
val index = name.indexOf('(');
if(index >= 0)
return name.substring(0, index);
return name;
}
} }
} }

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.platforms.js
class JSClientConstants {
companion object {
val PLUGIN_SPEC_VERSION = 2;
}
}

View file

@ -4,7 +4,9 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual
import java.net.URL import java.net.URL
import java.util.UUID import java.util.UUID
@ -31,6 +33,7 @@ class SourcePluginConfig(
override val allowEval: Boolean = false, override val allowEval: Boolean = false,
override val allowUrls: List<String> = listOf(), override val allowUrls: List<String> = listOf(),
override val packages: List<String> = listOf(), override val packages: List<String> = listOf(),
override val packagesOptional: List<String> = listOf(),
val settings: List<Setting> = listOf(), val settings: List<Setting> = listOf(),
@ -45,7 +48,12 @@ class SourcePluginConfig(
var enableInSearch: Boolean = true, var enableInSearch: Boolean = true,
var enableInHome: Boolean = true, var enableInHome: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(), var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
var changelog: HashMap<String, List<String>>? = null
) : IV8PluginConfig { ) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl); val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@ -75,16 +83,59 @@ class SourcePluginConfig(
private var _allowUrlsLowerVal: List<String>? = null; private var _allowUrlsLowerVal: List<String>? = null;
private val _allowUrlsLower: List<String> get() { private val _allowUrlsLower: List<String> get() {
if(_allowUrlsLowerVal == null) if(_allowUrlsLowerVal == null)
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }; _allowUrlsLowerVal = allowUrls.map { it.lowercase() }
.filter { it.length > 0 };
return _allowUrlsLowerVal!!; return _allowUrlsLowerVal!!;
}; };
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
//New allow header access
if(!allowAllHttpHeaderAccess && newConfig.allowAllHttpHeaderAccess)
return false;
//All urls should already be allowed
for(url in newConfig.allowUrls) {
if(!allowUrls.contains(url))
return false;
}
//All packages should already be allowed
for(pack in newConfig.packages) {
if(!packages.contains(pack))
return false;
}
for(pack in newConfig.packagesOptional) {
if(!packagesOptional.contains(pack))
return false;
}
//Developer Submit Url should be same or empty
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
return false;
//Should have a public key
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
return false;
//Should be same public key
if(scriptPublicKey != newConfig.scriptPublicKey)
return false;
//Old signature should be valid
if(!validate(oldScript))
return false;
//New signature should be valid
if(!newConfig.validate(newScript))
return false;
return true;
}
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> { fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
val list = mutableListOf<Pair<String,String>>(); val list = mutableListOf<Pair<String,String>>();
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id); val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
if (currentlyInstalledPlugin != null) { if (currentlyInstalledPlugin != null) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) { if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
list.add(Pair( list.add(Pair(
"Different Author", "Different Author",
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor.")); "This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
@ -107,6 +158,11 @@ class SourcePluginConfig(
list.add(Pair( list.add(Pair(
"Unrestricted Web Access", "Unrestricted Web Access",
"This plugin requires access to all URLs, this may include malicious URLs.")); "This plugin requires access to all URLs, this may include malicious URLs."));
if(allowAllHttpHeaderAccess)
list.add(Pair(
"Unrestricted Http Header access",
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
))
return list; return list;
} }
@ -125,7 +181,20 @@ class SourcePluginConfig(
return true; return true;
val uri = Uri.parse(url); val uri = Uri.parse(url);
val host = uri.host?.lowercase() ?: ""; val host = uri.host?.lowercase() ?: "";
return _allowUrlsLower.any { it == host }; return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
}
fun getChangelogString(version: String): String?{
if(changelog == null || !changelog!!.containsKey(version))
return null;
val changelog = changelog!![version]!!;
if(changelog.size > 1) {
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
}
else if(changelog.size == 1) {
return "Changelog (${version})\n" + changelog[0].trim();
}
return null;
} }
companion object { companion object {

View file

@ -8,6 +8,7 @@ import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.views.fields.DropdownFieldOptions import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -90,8 +91,10 @@ class SourcePluginDescriptor {
@Serializable @Serializable
class AppPluginSettings { class AppPluginSettings {
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1) @FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
var checkForUpdates: Boolean = true; var checkForUpdates: Boolean = true;
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
var automaticUpdate: Boolean = false;
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2) @FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
var tabEnabled = TabEnabled(); var tabEnabled = TabEnabled();
@ -130,6 +133,11 @@ class SourcePluginDescriptor {
} }
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
var allowDeveloperSubmit: Boolean = false;
fun loadDefaults(config: SourcePluginConfig) { fun loadDefaults(config: SourcePluginConfig) {
if(tabEnabled.enableHome == null) if(tabEnabled.enableHome == null)
tabEnabled.enableHome = config.enableInHome tabEnabled.enableHome = config.enableInHome

View file

@ -14,6 +14,6 @@ annotation class JSOptional()
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0) annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false); data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class JSParameterDocs(val name: String, val description: String); data class JSParameterDocs(val name: String, val description: String);

View file

@ -2,14 +2,22 @@ package com.futo.platformplayer.api.media.platforms.js.internal
import android.net.Uri import android.net.Uri
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.developer.DeveloperEndpoints
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StateDeveloper
import com.google.common.net.MediaType
import okhttp3.OkHttpClient
import okio.GzipSource
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.UUID import java.util.UUID
class JSHttpClient : ManagedHttpClient { class JSHttpClient : ManagedHttpClient {
@ -28,7 +36,15 @@ class JSHttpClient : ManagedHttpClient {
private var _currentCookieMap: HashMap<String, HashMap<String, String>>; private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
private var _otherCookieMap: HashMap<String, HashMap<String, String>>; private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() { constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
//Temporary ugly solution for DevPortal proxy support
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && StateDeveloper.instance.devProxy != null)
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
))
else
OkHttpClient.Builder())
) {
_jsClient = jsClient; _jsClient = jsClient;
_jsConfig = config; _jsConfig = config;
_auth = auth; _auth = auth;
@ -201,6 +217,16 @@ class JSHttpClient : ManagedHttpClient {
} }
} }
} }
if(_jsClient is DevJSClient) {
//val peekBody = resp.peekBody(1000 * 1000).string();
StateDeveloper.instance.addDevHttpExchange(
StateDeveloper.DevHttpExchange(
StateDeveloper.DevHttpRequest(resp.request.method, resp.request.url.toString(), mapOf(*resp.request.headers.map { Pair(it.first, it.second) }.toTypedArray()), ""),
StateDeveloper.DevHttpRequest("RESP", resp.request.url.toString(), mapOf(*resp.headers.map { Pair(it.first, it.second) }.toTypedArray()), "", resp.code)
));
}
return resp; return resp;
} }

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer.api.media.platforms.js.models package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
@ -26,6 +27,7 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj); ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj); ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj); ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj)
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }

View file

@ -16,6 +16,7 @@ interface IJSContentDetails: IPlatformContent {
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj); ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj); ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }

View file

@ -0,0 +1,162 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
val rating: IRating;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformPost";
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
else
thumbnails = null;
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
?.map { fromV8Segment(client, it) }
?.filterNotNull() ?: listOf());
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
return@handleDevCall getCommentsJS(client);
}
else if(client is JSClient)
return getCommentsJS(client);
return null;
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client);
}
else if(client is JSClient)
return getContentRecommendationsJS(client);
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
companion object {
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field");
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null;
}
}
}
}
enum class SegmentType(val value: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2),
NESTED(9);
companion object {
fun fromInt(value: Int): SegmentType
{
val result = SegmentType.entries.firstOrNull { it.value == value };
if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value");
return result;
}
}
}
interface IJSArticleSegment {
val type: SegmentType;
}
class JSTextSegment: IJSArticleSegment {
override val type = SegmentType.TEXT;
val textType: TextType;
val content: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
}
}
class JSImagesSegment: IJSArticleSegment {
override val type = SegmentType.IMAGES;
val images: List<String>;
val caption: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
}
}
class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED;
val nested: IPlatformContent;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSNestedSegment";
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
nested = IJSContent.fromV8(client, nestedObj);
}
}

View file

@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrDefaultList import com.futo.platformplayer.getOrDefaultList
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
@ -37,7 +38,7 @@ class JSChannel : IPlatformChannel {
description = _channel.getOrThrowNullable(config, "description", contextName); description = _channel.getOrThrowNullable(config, "description", contextName);
url = _channel.getOrThrow(config, "url", contextName); url = _channel.getOrThrow(config, "url", contextName);
urlAlternatives = _channel.getOrDefaultList(config, "urlAlternatives", contextName, listOf()) ?: listOf(); urlAlternatives = _channel.getOrDefaultList(config, "urlAlternatives", contextName, listOf()) ?: listOf();
links = HashMap(); links = HashMap(_channel.getOrDefault<Map<String, String>>(config, "links", contextName, mapOf()) ?: mapOf());
} }
override fun getContents(client: IPlatformClient): IPager<IPlatformContent> { override fun getContents(client: IPlatformClient): IPager<IPlatformContent> {

View file

@ -5,7 +5,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> { class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {

View file

@ -42,10 +42,15 @@ open class JSContent : IPlatformContent, IPluginSourced {
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName)); id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString(); name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong(); val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
if(datetimeInt == 0.toLong()) if(authorObj != null)
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
else
author = PlatformAuthorLink.UNKNOWN;
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
if(datetimeInt == null || datetimeInt == 0.toLong())
datetime = null; datetime = null;
else else
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC); datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
@ -54,4 +59,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
_hasGetDetails = _content.has("getDetails"); _hasGetDetails = _content.has("getDetails");
} }
fun getUnderlyingObject(): V8ValueObject? {
return _content;
}
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@ -15,4 +16,14 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override fun convertResult(obj: V8ValueObject): IPlatformContent { override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(plugin, obj); return IJSContent.fromV8(plugin, obj);
} }
}
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return JSChannelContent(config, obj);
}
} }

View file

@ -14,12 +14,13 @@ import java.time.ZoneOffset
class JSLiveChatWindowDescriptor: ILiveChatWindowDescriptor { class JSLiveChatWindowDescriptor: ILiveChatWindowDescriptor {
override val url: String; override val url: String;
override val removeElements: List<String>; override val removeElements: List<String>;
override val removeElementsInterval: List<String>;
constructor(config: SourcePluginConfig, obj: V8ValueObject) { constructor(config: SourcePluginConfig, obj: V8ValueObject) {
val contextName = "LiveChatWindowDescriptor"; val contextName = "LiveChatWindowDescriptor";
url = obj.getOrThrow(config, "url", contextName); url = obj.getOrThrow(config, "url", contextName);
removeElements = obj.getOrDefault(config, "removeElements", contextName, listOf()) ?: listOf(); removeElements = obj.getOrDefault(config, "removeElements", contextName, listOf()) ?: listOf();
removeElementsInterval = obj.getOrDefault(config, "removeElementsInterval", contextName, listOf()) ?: listOf();
} }
} }

View file

@ -71,6 +71,8 @@ abstract class JSPager<T> : IPager<T> {
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager"); val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed");
val newResults = items.toArray() val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) } .map { convertResult(it as V8ValueObject) }
.toList(); .toList();

View file

@ -17,6 +17,8 @@ class JSPlaybackTracker: IPlaybackTracker {
private var _lastRequest: Long = Long.MIN_VALUE; private var _lastRequest: Long = Long.MIN_VALUE;
private val _hasOnConcluded: Boolean;
override var nextRequest: Int = 1000 override var nextRequest: Int = 1000
private set; private set;
@ -26,6 +28,7 @@ class JSPlaybackTracker: IPlaybackTracker {
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker"); throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
if(!obj.has("nextRequest")) if(!obj.has("nextRequest"))
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker"); throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
_hasOnConcluded = obj.has("onConcluded");
this._config = config; this._config = config;
this._obj = obj; this._obj = obj;
@ -59,6 +62,16 @@ class JSPlaybackTracker: IPlaybackTracker {
} }
} }
} }
override fun onConcluded() {
warnIfMainThread("JSPlaybackTracker.onConcluded");
if(_hasOnConcluded) {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
_obj.invokeVoid("onConcluded", -1);
}
}
}
override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest); override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest);
} }

View file

@ -14,6 +14,6 @@ open class JSPlaylist : JSContent, IPlatformPlaylist {
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) { constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Playlist"; val contextName = "Playlist";
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null); thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
videoCount = obj.getOrDefault(config, "videoCount", contextName, 0)!!; videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
} }
} }

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