Compare commits

...

1455 Commits

Author SHA1 Message Date
sarendsen
985d407891 fix: loading conditionals 2025-06-07 12:36:26 +02:00
Fredrik Burmester
68b5fe3599 chore 2025-06-03 08:20:43 +02:00
Fredrik Burmester
67f73bfa39 fix: format 2025-06-03 08:20:35 +02:00
Fredrik Burmester
5de7cab285 Merge branch 'master' into develop 2025-06-03 08:20:32 +02:00
Fredrik Burmester
67d39c39ea fix: android bug 2025-06-03 08:06:49 +02:00
Gauvain
9d8e227609 fix: remove description of pr in message (#737) 2025-06-02 16:28:28 +02:00
renovate[bot]
962323a75c chore(deps): update github/codeql-action action to v3.28.18 (#727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 16:18:42 +02:00
Gauvain
fc23201b4f fix: biome check, remove spell-check (#731) 2025-06-02 16:17:34 +02:00
lance chant
f0519ea88d fix: tv home screen navigation (#732) 2025-06-02 15:15:31 +02:00
renovate[bot]
f9f21606ff chore(deps): pin dependencies (#722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 14:18:04 +02:00
Gauvain
c4d026f4d8 fix: remove cache bun (#730) 2025-06-02 14:15:46 +02:00
Gauvain
577827303e fix: correct name of dictonnary and use correct version in action (#728) 2025-06-02 14:13:44 +02:00
Gauvain
3e0a1af9fa fix: fix error on pr title for renovate bot (#729) 2025-06-02 14:13:36 +02:00
lance chant
63bc806a06 fix: made tv os compile (#721) 2025-06-02 14:13:13 +02:00
Jaakko Rantamäki
f05496a458 feat: Adaptive icons for iOS 18 and Android (#606) 2025-06-02 14:04:41 +02:00
Gauvain
d3660b45b1 fix: rename merge conflict label (#725) 2025-06-02 13:52:12 +02:00
Sim
1b812ebed5 fix: Recently Added isn't updating correctly. (#686) 2025-06-02 13:21:50 +02:00
Chris
6703299da9 docs: fix typo in README.md (#719) 2025-06-02 13:17:37 +02:00
Kamil Kosek
80d63c0219 feat: remotecontrol (#705) 2025-06-02 13:16:15 +02:00
Nyanmisaka
c2f8145e74 fix: Fixed container name mp4 in transcoding profiles (#696) 2025-06-02 13:14:31 +02:00
Gauvain
ce00aeb5f1 feat(ci/cd): Add Android builds and quality checks (#694) 2025-06-02 13:14:20 +02:00
Fredrik Burmester
5899cc8625 chore: version code 2025-05-30 21:00:02 +02:00
sarendsen
90217bb495 fix: error race conditions 2025-05-29 16:39:46 +02:00
lostb1t
16e88cca8c fix: error race conditions 2025-05-29 16:04:22 +02:00
lostb1t
e8e62061ae fix: remove unwanted detail calls on search (#707) 2025-05-28 14:24:22 +02:00
retardgerman
3adc4d2a21 fix(lang): uk.json 2025-05-19 13:44:19 +02:00
Chris
185524c06c feat(lang): add Klingon and Esperanto localization support (#672) 2025-05-19 12:39:02 +02:00
Simon Eklundh
6a208ee201 fix: improve readme to reduce the questions we tend to receive rather… (#699) 2025-05-18 20:36:08 +02:00
Ahmed Sbai
99938ddf5a feat: add "Are you still watching" modal overlay with configurable options (#663)
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
2025-05-18 09:21:50 +02:00
lostb1t
963a54a36c fix: cancel for direct downloads 2025-05-14 21:18:48 +02:00
sarendsen
e939c9b933 Revert "style: horizontal width"
This reverts commit 31f662a582.
2025-05-14 21:07:18 +02:00
lostb1t
2ffd569bba chore: fix build 2025-05-14 19:42:56 +02:00
storm1er
c8ea494d6f fix: downgrade expo-sharing version until expo 53 (#688) 2025-05-14 19:18:46 +02:00
Danylo Kozhushko
577a61a452 fix: Fixed Ukrainian translation filename and also some typos (#682) 2025-05-14 19:09:54 +02:00
retardgerman
a731c4eebd fix: remove Feature that doesn’t exist. 2025-05-12 20:55:37 +02:00
Fredrik Burmester
8a664757b8 fix: android popup crash patch 2025-05-05 11:19:06 +02:00
sarendsen
655a78900d fix: try to enable ios background downloads plugin 2025-05-04 18:38:27 +02:00
sarendsen
87a33af8d1 fix: restore downloads if missing 2025-05-04 18:12:16 +02:00
sarendsen
36b1c48fdd fix: use ts for downloads 2025-05-04 12:50:21 +02:00
lostb1t
0454ba9f29 Update DownloadProvider.tsx 2025-05-04 12:01:51 +02:00
lostb1t
b55ed6349c Update DownloadProvider.tsx 2025-05-04 11:56:47 +02:00
Ryan
0c34add45a fix: update search functionality to set text in search bar on press (#669) 2025-05-04 11:48:53 +02:00
lostb1t
1c1345a3b7 feat: move to custom download handler with background download support (#675) 2025-05-04 11:46:34 +02:00
Chris
9f706a348e chore: Update README.md - Sessions View (#673) 2025-05-03 17:29:51 +02:00
sarendsen
f4750e781d refactor: getstreamurl 2025-05-02 19:02:35 +02:00
lance chant
0b574cc047 fix: dolby vision on supported devices, specifically profile 5 (#660) 2025-05-01 12:11:29 +02:00
Alec Warren
4a816470d1 feat: improve jellyseer item page buttons (#634) 2025-04-29 18:40:43 +02:00
Ryan
0d43b57f55 fix: improve empty state layout in library view (#665) 2025-04-28 18:11:24 +02:00
sarendsen
31f662a582 style: horizontal width 2025-04-21 12:28:12 +02:00
Alex
23e0ec9774 Remove Alamofire (#656) 2025-04-20 00:25:51 +10:00
Alex
d6ac8569a8 Fix/external subtitle support vlc3 (#655) 2025-04-20 00:25:35 +10:00
Gap
2ce04b3fd3 feat(lang): Added Russian localization (#613) 2025-04-11 20:08:09 +02:00
Leonardo
bf5203348b feat: add portuguese (pt-BR) translation (#625) 2025-04-11 18:06:41 +02:00
lance chant
16fb1a52ca fix: Fixed the import of expo-application to be the as expo docs and it allowed application to be populated (#648) 2025-04-11 17:56:29 +02:00
sarendsen
d8be7b2463 fix: disable downloads for the moment 2025-04-10 19:39:05 +02:00
sarendsen
ec37b5ab2c fix: use ffmpegkit fork 2025-04-10 16:30:55 +02:00
sarendsen
29eb072e5d fix: use items endpoint for search 2025-04-08 12:32:43 +02:00
sarendsen
2a4a7f5f2d fix: return of export log 2025-04-07 14:33:03 +02:00
sarendsen
8b3f950bc5 fix: use ffmpegkit fork 2025-04-07 12:44:39 +02:00
sarendsen
db527311d6 fix: use ffmpegkit fork 2025-04-07 10:44:45 +02:00
lance chant
b76e834be1 feat: adding reportPlaybackStart which allows tracking to work well (#636) 2025-04-06 10:24:33 +02:00
herrrta
c9905d9d88 fix: add null safety to default folder path 2025-04-01 19:31:27 -04:00
Ahmed Sbai
b9bb109f4a chore: linting fixes && github actions for linting (#612) 2025-03-31 07:44:10 +02:00
Fredrik Burmester
16b834cf71 fix: lint issues 2025-03-30 10:21:41 +02:00
herrrta
f6baf490fb chore: add environment names to builds 2025-03-29 11:42:52 -04:00
herrrta
3201499397 chore: add export log string 2025-03-29 10:52:45 -04:00
herrrta
6555251c2e feat: expo env variables & export logs 2025-03-29 10:44:28 -04:00
sarendsen
71c15f3651 feat: Implement latest for custom home 2025-03-29 14:47:38 +01:00
herrrta
25da30d6e2 fix: env variable 2025-03-28 19:16:16 -04:00
herrrta
1394eae01e feat: better logs
- added ability to write debug logs for development builds
- added filtering to log page
- modified filter button to allow for multiple selection if required
2025-03-28 19:11:36 -04:00
Fredrik Burmester
205715ae29 chore 2025-03-25 13:13:46 +01:00
herrrta
587d419502 feat: new notification deep links 2025-03-21 20:52:22 -04:00
herrrta
bc081b535e chore: version bump 2025-03-21 20:22:31 -04:00
herrrta
62d2d1f7ca fix: generate expo push tokens for real device only 2025-03-19 19:17:22 -04:00
lostb1t
66aab5b771 Update README.md 2025-03-19 12:14:00 +01:00
lostb1t
5c89100afd Update README.md 2025-03-19 11:58:08 +01:00
Fredrik Burmester
ffbaaa81a8 Merge branch 'develop' 2025-03-19 11:33:51 +01:00
Fredrik Burmester
f1a3b48017 chore 2025-03-19 11:33:27 +01:00
Fredrik Burmester
8ab72b1262 chore 2025-03-17 10:11:25 +01:00
Fredrik Burmester
b9c02618d5 fix: intentionally commit the google-services file - not a secret 2025-03-17 10:11:22 +01:00
Fredrik Burmester
0b22f28bb6 fix: env 2025-03-17 09:56:24 +01:00
Fredrik Burmester
2932a7b324 chore 2025-03-17 09:56:18 +01:00
Fredrik Burmester
8e8ae32287 fix: remove patch 2025-03-17 09:45:51 +01:00
Fredrik Burmester
2189b3d3dd fix: android push notifications 2025-03-16 21:31:33 +01:00
Fredrik Burmester
f770cf174b fix: linting config 2025-03-16 18:12:10 +01:00
Fredrik Burmester
f7e771123f fix: lint config 2025-03-16 18:09:53 +01:00
Fredrik Burmester
5757b1c010 fix: lint 2025-03-16 18:08:55 +01:00
lostb1t
92513e234f chore: Apply linting rules and add git hok (#611)
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
2025-03-16 18:01:12 +01:00
Ahmed Sbai
2688e1b981 feat: add Polish translation and update language options (#608) 2025-03-16 17:57:39 +01:00
Ahmed Sbai
54423a1267 fix: fixed app crash on next downloaded item && update biome schema v… (#610) 2025-03-16 17:57:25 +01:00
lostb1t
defe87debb fix: disable badge count for sessions 2025-03-16 17:19:48 +01:00
Ahmed Sbai
a1b2248f16 chore: add biome configuration (#590) 2025-03-16 08:12:22 +01:00
Ahmed Sbai
cd42a86d40 feat: enhance favorites with empty cell && added translations (#594) 2025-03-15 14:19:12 -04:00
Fredrik Burmester
76661c7599 Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-03-15 19:17:15 +01:00
Fredrik Burmester
6de829c16d fix: owner of app moved to org 2025-03-15 19:17:13 +01:00
herrrta
9b0ba285b3 feat: Ability to consume webhook notifications and forward to clients #595
- forward expo device tokens to users plugin instance
- added android notification icon
2025-03-15 14:14:38 -04:00
Danylo Kozhushko
cbcb160bdd feat: Added Ukrainian translation (#593) 2025-03-15 09:21:52 +01:00
Ahmed Sbai
10bfa95060 fix: update textContentType for username input to oneTimeCode (#587) 2025-03-15 09:21:24 +01:00
Chris
7201be6f02 Update README.md (#605) 2025-03-14 14:47:09 +01:00
sarendsen
9f17f13175 refactor: remove tv version of home 2025-03-13 17:00:24 +01:00
sarendsen
c0e9f29c04 fix: home refresh 2025-03-13 16:41:43 +01:00
sarendsen
ab5df3c9ef fix: limit item titel to oneline 2025-03-13 15:57:56 +01:00
herrrta
7768939767 fix: NPE when unregistering receiver 2025-03-11 15:08:48 -04:00
Fredrik Burmester
8b72bde4a9 Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-03-11 07:21:55 +01:00
sarendsen
c29b2cb8da feat: Add location to session 2025-03-10 15:51:18 +01:00
Fredrik Burmester
96e3362f43 fix: rotate bugs in video player 2025-03-08 10:33:14 +01:00
Fredrik Burmester
7cdf0e5355 chore 2025-03-08 10:13:04 +01:00
Fredrik Burmester
ef355b1f04 fix: remove unused react-native-video 2025-03-08 08:27:28 +01:00
Fredrik Burmester
81535894e1 fix: unwanted rotate to portrait after a long time of watching 2025-03-08 08:27:18 +01:00
Fredrik Burmester
887f30e739 fix: rename auto rotate to follow device orientation 2025-03-07 07:54:13 +01:00
Fredrik Burmester
3d7889e19a fix: orientation lock being activated even when auto rotate is on 2025-03-07 07:53:50 +01:00
herrrta
4b8e8cddb5 fix: Flashlist container not changing height 2025-03-05 20:23:28 -05:00
herrrta
d33baf07d3 fix: Recent requests requester name 2025-03-05 20:16:42 -05:00
herrrta
66f61c3c38 fix: Recent request slider initial loading 2025-03-05 20:07:21 -05:00
herrrta
88efb09317 fix: fix jellyseerr search results 2025-03-05 19:45:36 -05:00
lostb1t
5df021a836 feat: Add session count to app badge (#575) 2025-03-05 08:21:59 +01:00
Ahmed Sbai
89eb0d7796 fix(https://github.com/streamyfin/streamyfin/issues/566): add Turkish (#583) 2025-03-05 08:14:34 +01:00
herrrta
ba9178a0f6 fix: Jellyseerr dont compare against tv original names during sorting 2025-03-05 01:30:42 -05:00
herrrta
27cd73efab fix: Jellyseerr slider bottom padding for posters 2025-03-05 01:24:09 -05:00
herrrta
e15b19deb3 fix: Jellyseerr filter buttons showing when library selected 2025-03-05 01:06:39 -05:00
herrrta
baccc931a2 fix: Jellyseerr order by "default" not preselected (ui)
- ui just didnt reflect this
2025-03-05 00:47:47 -05:00
herrrta
79a2873975 fix: Fix search item count translation 2025-03-05 00:35:54 -05:00
herrrta
e397be4b2e feat: Better Jellyseerr search results #586
- fetch 4 pages at once to maximize search results
- add local sorting options
2025-03-05 00:32:30 -05:00
herrrta
4dddc0f926 fix: Jellyseerr Recent Request slide fixes
- added src for backdrop + poster
- fixed horizontal height issues
2025-03-04 21:09:15 -05:00
Fredrik Burmester
ebcb414b89 fix: use sdk util 2025-03-03 16:10:47 +01:00
Fredrik Burmester
77dba04289 fix 2025-03-03 16:06:48 +01:00
lostb1t
12ceef02cd fix: mark as played 2025-03-03 16:01:27 +01:00
Fredrik Burmester
fe3b652b4f fix: more specified dep arr 2025-03-03 15:21:18 +01:00
Fredrik Burmester
9c9785ba9e chore 2025-03-03 15:20:23 +01:00
Fredrik Burmester
bce9ed2690 fix: wrong prop 2025-03-03 15:20:13 +01:00
Fredrik Burmester
ec914133d6 fix: undefined var 2025-03-03 15:19:59 +01:00
Fredrik Burmester
2d1b03e403 Merge branch 'feat/switch-players' into develop 2025-03-03 15:18:20 +01:00
Fredrik Burmester
7f07260177 fix: hide setting (use only vlc3 for now) 2025-03-03 15:18:09 +01:00
herrrta
e1314077e2 fix: only show recent requests when request is done loading 2025-03-03 01:25:58 -05:00
herrrta
09e9462ac0 feat: (iOS) Switch Video Players 2025-03-03 01:12:08 -05:00
herrrta
dd65505f7f feat: [Jellyseerr] Show recent requests #324
- Added recent requests slide
- updated JellyseerrPoster.tsx to handle more options
2025-03-03 01:04:49 -05:00
herrrta
951158bcd3 fix: Discover page key collisions #581
- add uniqBy for jellyseerr results
- add missing key in MovieTvSlide.tsx
2025-03-02 13:07:14 -05:00
herrrta
9b1dd0923a fix: Don't show all seasons numbers in request modal [Jellyseerr] #580 2025-03-02 12:38:58 -05:00
herrrta
bd908516b5 fix: Advanced request options not saving #543 2025-03-02 12:21:24 -05:00
lostb1t
8cb10d1062 Update _layout.tsx 2025-03-01 09:37:50 +01:00
lostb1t
446439c2e0 Update package.json 2025-02-28 00:11:03 +01:00
lostb1t
a5463d783d fix: use correct url on save for optimized 2025-02-26 19:25:20 +01:00
Little709
640db35456 fix: Update nl.json (#565) 2025-02-26 14:21:42 +01:00
Simon Eklundh
caa4b765c1 fix: makes the icon adaptive for android (#569) 2025-02-26 08:23:27 +01:00
sarendsen
9c6aebe66a small cleanup 2025-02-24 18:41:07 +01:00
sarendsen
ef42510383 small cleanup 2025-02-24 14:56:39 +01:00
sarendsen
5273dfd22b small cleanup 2025-02-24 14:23:48 +01:00
Fredrik Burmester
00bc4232fb fix: xcode warnings 2025-02-24 11:51:48 +01:00
sarendsen
35c9258062 fix: playback pause/play reporting 2025-02-24 10:30:01 +01:00
sarendsen
89bf51c3cc fix: playback reporting 2025-02-24 09:30:14 +01:00
sarendsen
f64c5a02db fix: add hw/sw badge to session 2025-02-23 19:15:10 +01:00
sarendsen
cf284eb3d8 fix: sort sessions by name 2025-02-23 18:42:00 +01:00
Alex
b581a077e1 General refactoring (#559) 2025-02-23 09:40:10 -05:00
Fredrik Burmester
e651b975b7 fix: ts error 2025-02-23 15:08:14 +01:00
lostb1t
1c550b1b77 feat: Mark/unmark favorite quick action (#561) 2025-02-23 15:03:52 +01:00
Ahmed Sbai
5bcae81538 fix(511): fixed long named translations for the subtitle can break UI (#564) 2025-02-23 15:03:22 +01:00
Fredrik Burmester
c951725222 chore 2025-02-23 15:02:50 +01:00
Fredrik Burmester
0b966d7c04 fix: add hevc to chromecast h265 profile 2025-02-23 14:50:14 +01:00
Fredrik Burmester
8e0e35afe3 fix: chromecast 2025-02-23 14:35:00 +01:00
Fredrik Burmester
daf7f35196 feat: scroll to top on tab home press 2025-02-22 13:23:33 +01:00
Fredrik Burmester
d5ac30b6d8 feat: scroll to top on tab home press 2025-02-22 13:20:52 +01:00
Fredrik Burmester
81b91bbb97 chore 2025-02-22 13:11:37 +01:00
Fredrik Burmester
af2bd030e9 feat: focus search bar on second tab press (#558) 2025-02-22 13:09:17 +01:00
Fredrik Burmester
5590c2f784 fix: added season and episode + updated icon 2025-02-22 12:08:58 +01:00
Fredrik Burmester
6cc70dd123 fix: type issues 2025-02-22 12:02:33 +01:00
Edmond
fae588b0f0 fix: Improve Chinese (Traditional) Translation (#557) 2025-02-22 11:06:43 +01:00
vuhe
bd2aeb2234 feat: Add Chinese (Simplified) Translation (#556) 2025-02-22 11:06:10 +01:00
sarendsen
cca0bbf42c bigger play button 2025-02-22 10:58:28 +01:00
Fredrik Burmester
06e0eb5c4e Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-02-21 20:38:45 +01:00
Fredrik Burmester
b478fbb6bf fix: tvos fixes 2025-02-21 20:38:31 +01:00
lostb1t
b98a7b0634 Update _layout.tsx 2025-02-21 18:22:05 +01:00
lostb1t
ce38024a3f Update settings.tsx 2025-02-21 14:57:53 +01:00
lostb1t
04dce9265b Update _layout.tsx 2025-02-21 14:56:59 +01:00
lostb1t
5b8418cd82 feat: Sessions view (#537) 2025-02-21 13:14:57 +01:00
tkymmm
b0c5255bd7 feat: add japanese translations (#552) 2025-02-21 11:09:36 +01:00
Fredrik Burmester
73dd171987 chore: version bump 2025-02-20 16:30:36 +01:00
Ahmed Sbai
ff35559687 fix: remove unused imports and optimize keepAwake usage in the player (#548) 2025-02-20 11:18:37 +01:00
Fredrik Burmester
5aadd50946 chore 2025-02-20 11:16:10 +01:00
herrrta
63b5ba2112 feat: add upcoming air dates for episodes 2025-02-19 21:49:28 -05:00
herrrta
8b955578a2 fix: Jellyseerr url input 2025-02-19 21:07:50 -05:00
herrrta
1e5c021c93 fix: Reset ios vout when media is not playing 2025-02-19 20:58:39 -05:00
Fredrik Burmester
0b86f56486 fix: spelling 2025-02-19 20:36:51 +01:00
Fredrik Burmester
728b93f4e5 fix: up stale issue time to 90 days, ignore feature requests 2025-02-19 20:36:31 +01:00
Fredrik Burmester
2fc483b24e Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-02-19 20:19:39 +01:00
Fredrik Burmester
fc901bc01e fix: jellyseerr login without password 2025-02-19 20:19:34 +01:00
lostb1t
2b0884b154 Update README.md 2025-02-19 17:14:01 +01:00
lostb1t
307d20e538 Update README.md 2025-02-19 17:13:12 +01:00
Fredrik Burmester
a2f03908f6 fix: remove splashscreen provider and handle loading in jellyfinprovider 2025-02-19 14:44:39 +01:00
Fredrik Burmester
77aef8877e fix: jellyseerr better login 2025-02-19 11:36:47 +01:00
Fredrik Burmester
0cf930d6e1 test: fix controls loading android? 2025-02-19 11:06:57 +01:00
Fredrik Burmester
4b0b949541 fix: button alignment pip 2025-02-19 10:55:29 +01:00
Fredrik Burmester
14b717f985 fix: half screen black on login 2025-02-19 10:54:05 +01:00
Fredrik Burmester
cfbac538f8 chore: refactor for tv stuff 2025-02-19 10:49:18 +01:00
Fredrik Burmester
1ac6b7e3df fix: chromecast not working 2025-02-18 17:56:10 +01:00
Davide Sirico
c9f6e8676b feat: add italian translations (#545) 2025-02-18 16:10:15 +01:00
Fredrik Burmester
5aab1450cd chore 2025-02-18 16:06:35 +01:00
Fredrik Burmester
1e7080a136 chore 2025-02-18 16:06:32 +01:00
Théo FORTIN
993cec4138 feat: remove stale issues (#515) 2025-02-17 16:54:25 +01:00
Ahmed Sbai
6c524499f9 chore: remove async-storage && moved @types/xxx dependencies to dev-deps (#538) 2025-02-17 16:54:12 +01:00
Maarten
b3463ffdfc feat: add dutch translations (#539)
Co-authored-by: Maarten Schroeven <maarten.schroeven@ae.be>
2025-02-17 16:53:10 +01:00
Edmond
50942b44f1 feat: Add Chinese (Traditional) Translation (#522) 2025-02-17 16:52:58 +01:00
Mustafa
f602f8919f fix: translation de.json grammar (#516) 2025-02-17 16:52:44 +01:00
herrrta
0e86d8a00f fix: IOS video player black screens pt2
- Looks like re-adding subview was not enough. We have to toggle the video tracks selection and play the media to trigger the re-render
2025-02-16 15:05:32 -05:00
Adrián
56b1e1977c fix: Change too long texts in the Spanish translation (#535) 2025-02-16 13:22:13 +01:00
herrrta
30e23b9079 fix: IOS video player black screens
- restores player view when re-entering apps foreground
- added logger
2025-02-15 22:18:16 -05:00
herrrta
d83ecb881b fix: Android PiP support fully working
- fixed black screen on re-entering
- ensured screen stays alive when video is playing
- PiP button states now reflect media status
2025-02-15 15:11:57 -05:00
Fredrik Burmester
4c14c08b35 fix: move from react-native-video -> VLC for transcoded streams (#529)
Co-authored-by: Alex Kim <alexkim5682@gmail.com>
2025-02-16 07:10:36 +11:00
herrrta
ecb9b90163 fix: Stop playback when gesture navigating back 2025-02-15 12:52:09 -05:00
Fredrik Burmester
33a2be24f4 fix: hidden be default ios 2025-02-15 12:00:58 +01:00
Fredrik Burmester
e8b0d52515 feat: change to native searchbar on android 2025-02-15 11:59:17 +01:00
Fredrik Burmester
9faa0de2d6 chore: bump version 2025-02-15 11:48:54 +01:00
Fredrik Burmester
221155d002 fix: deps 2025-02-15 11:34:50 +01:00
Fredrik Burmester
4a37e17324 chore 2025-02-14 13:28:06 +01:00
Fredrik Burmester
52b2a3418e fix: wrong deps 2025-02-13 10:35:58 +01:00
Fredrik Burmester
2753b243e5 chore: remove yarn lock file 2025-02-13 10:33:56 +01:00
Fredrik Burmester
f22b356b7c feat: turkish translations 2025-02-13 10:33:41 +01:00
Fredrik Burmester
d8ba5af8d9 chore: remove old patch 2025-02-13 10:33:34 +01:00
herrrta
505ef39ee7 ios VLCKit 4.0 & All platform PiP support 2025-02-12 23:21:24 -05:00
Théo FORTIN
e71d5cc176 feat: Add default quality setting (#509) 2025-02-12 08:32:26 +01:00
Fredrik Burmester
74e57bbd88 fix: add contributor avatars to readme (#512) 2025-02-12 08:31:55 +01:00
Fredrik Burmester
76eaeb9820 chore 2025-02-12 08:31:38 +01:00
Fredrik Burmester
9a70f98dd5 chore 2025-02-12 08:25:36 +01:00
herrrta
f28f1d8736 Fix android discover page crash 2025-02-11 10:16:36 -05:00
lostb1t
e0f03ccb93 feat: Allow plugin override defaults (#508) 2025-02-10 17:38:01 +01:00
lostb1t
34d1dbb20e Update README.md 2025-02-10 15:39:14 +01:00
Simon Eklundh
e3e2db659d fix: download player (#506) 2025-02-09 13:40:45 +01:00
Fredrik Burmester
528b4ad7ac fix: orientation in video player and app i general 2025-02-09 11:45:32 +01:00
lostb1t
d29501386b chore: expo 52 (#502)
Co-authored-by: herrrta <73949927+herrrta@users.noreply.github.com>
2025-02-09 10:46:05 +01:00
Simon Eklundh
6688469b6c fix: fixes non-optimized downloads (#500) 2025-02-09 10:43:42 +01:00
lostb1t
ae9c30aa6d fix: fix home and header nav not showing (#499) 2025-02-08 17:48:05 +01:00
Fredrik Burmester
364d2e8a51 fix: typescript errors 2025-02-08 10:51:52 +01:00
herrrta
6cc90b46b3 TV: fix navigation on login (#494) 2025-02-07 21:57:13 -05:00
sarendsen
33adea2819 fix more import for tv 2025-02-07 14:22:54 +01:00
Simon Eklundh
9f41861dcf fix: download provider import usage so we can play again (#491) 2025-02-06 23:12:44 +01:00
lostb1t
2b2d23e574 Update README.md 2025-02-06 17:53:01 +01:00
lostb1t
f6e2bcb120 Update README.md 2025-02-06 17:52:02 +01:00
lostb1t
314cd62bee Update README.md 2025-02-06 17:48:25 +01:00
lostb1t
41e7123d1c Update README.md 2025-02-06 17:48:03 +01:00
lostb1t
2af42b39f5 Update README.md 2025-02-06 17:44:31 +01:00
lostb1t
0a06b336c8 Update network_security_config.xml 2025-02-06 12:37:17 +01:00
lostb1t
028c9159f3 Update eas.json 2025-02-06 09:38:38 +01:00
sarendsen
dee4fa07e3 refactor: playbutton for tv 2025-02-05 15:07:11 +01:00
lostb1t
2764f1736a Update eas.json 2025-02-05 13:58:30 +01:00
Fredrik Burmester
d3d1a7bcde Merge pull request #374 from streamyfin/feature/bigscreen
feat: Initial support for tvOs/AndroidTV
2025-02-05 13:41:19 +01:00
sarendsen
7fcd598fa1 wip 2025-02-05 10:04:50 +01:00
sarendsen
0fc1506b11 merge develop 2025-02-05 09:44:03 +01:00
Adrián
e0aa7ea0df fix: Change phone_usage key to device_usage in Spanish translations (#479) 2025-02-04 15:23:41 +01:00
Mustafa
25f77645f8 fix: phone_usage to device_usage due PR #456 for DE language (#478) 2025-02-02 13:05:58 +01:00
Gauvain
1c81091e8b fix(i18n): fix french translation and wrong keys (#456) 2025-02-02 09:20:08 +01:00
Mustafa
94502b558d feat: Add German Translation DE (#477) 2025-02-02 09:18:15 +01:00
Adrián
a7d7d00eb3 feat: Translate app to Spanish (#457) 2025-02-02 09:17:54 +01:00
Fredrik Burmester
3b5e07c1d2 chore 2025-02-01 10:14:09 +01:00
Fredrik Burmester
db10369fb5 chore 2025-02-01 09:29:05 +01:00
Fredrik Burmester
32da5918c7 chore 2025-01-31 15:57:03 +01:00
Fredrik Burmester
dc542021b5 chore 2025-01-31 15:47:03 +01:00
Fredrik Burmester
bfad157a28 Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-01-31 15:36:52 +01:00
Fredrik Burmester
a71a646743 chore 2025-01-31 15:36:49 +01:00
sarendsen
366bc0137e WIP 2025-01-31 13:22:51 +01:00
Tom Heidenreich
3eb60840e6 fix: Rendered more hooks than during the previous render in NextEpisodeCountDownButton (#475) 2025-01-31 10:14:59 +01:00
sarendsen
65c4a1340d WIP 2025-01-30 11:19:36 +01:00
sarendsen
3e90447dd4 WIP 2025-01-30 10:18:07 +01:00
sarendsen
bd0768797e WIP 2025-01-30 09:20:31 +01:00
Max Ward
730ef4616f feat: Mark entire seasons of a show as played (#445) 2025-01-29 10:54:00 +01:00
lostb1t
c4d4475aa9 Create lint-pr.yaml 2025-01-27 14:04:22 +01:00
Fredrik Burmester
d1eb40f2a9 chore 2025-01-27 13:26:23 +01:00
Fredrik Burmester
77518d774e chore 2025-01-27 13:00:40 +01:00
Fredrik Burmester
a6fb7b956d chore 2025-01-27 13:00:16 +01:00
Tom Heidenreich
034ff3f478 Feat/Show Splashcreen until UI loaded (#437) 2025-01-27 10:28:53 +01:00
Max Ward
98ca4e7a6d Fix mark as played sheet logic being reversed (#443) 2025-01-27 08:27:28 +01:00
herrrta
461a276a20 Merge pull request #461 from streamyfin/fix/460
fix: Requesting some seasons not working [Jellyseerr]
2025-01-25 15:06:08 -05:00
herrrta
3975473da9 fix: Requesting some seasons not working [Jellyseerr] 2025-01-25 15:05:48 -05:00
lostb1t
d34b86297a Update ScrollingCollectionList.tsx 2025-01-24 09:06:30 +01:00
sarendsen
c4a83e283f feat: hide sections when empty by default 2025-01-24 08:59:41 +01:00
sarendsen
dac471f0a6 feat: hide sections when empty by default 2025-01-24 08:55:02 +01:00
Fredrik Burmester
252fc4387b chore 2025-01-23 11:29:35 +01:00
Fredrik Burmester
3e299e2136 fix: early return causing crash 2025-01-23 10:07:21 +01:00
Fredrik Burmester
01cab2277e Merge pull request #451 from RodoMa92/add_self_signed_support
[android] Trust android local CA store for self signed certificates
2025-01-23 10:02:16 +01:00
Fredrik Burmester
e4f4e861e0 Merge pull request #340 from simoncaron/feat/i18n
Implement translation with i18next
2025-01-23 10:01:13 +01:00
Marco Rodolfi
4d665013f0 [android] Trust android local CA store for self signed certificates 2025-01-22 20:08:20 +01:00
sarendsen
9aa4ea4a2e refactor: home section lists 2025-01-22 07:27:08 +01:00
sarendsen
93ae03f55c fix #446 2025-01-20 10:51:39 +01:00
Fredrik Burmester
b311ac98a7 Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-01-17 07:43:23 +01:00
Fredrik Burmester
83d425b2fb chore 2025-01-17 07:43:04 +01:00
Simon Caron
007fbdd0a3 Merge branch 'develop' into feat/i18n 2025-01-16 20:38:00 -05:00
Simon Caron
37df999db5 Merge pull request #1 from Gauvino/fix-typo
fix(i18n): missing typo and comma
2025-01-16 20:35:46 -05:00
sarendsen
72b9675df4 feat: Implement nextup for custom home 2025-01-16 10:36:20 +01:00
lostb1t
7a30a63335 Update README.md 2025-01-15 09:13:03 +01:00
sarendsen
0ff0fab3f4 fix: fix horizontal shows 2025-01-15 00:47:10 +01:00
Fredrik Burmester
d9d9b0ee00 Merge pull request #430 from streamyfin/feat/refreshsettings
feat: Refresh remote settings
2025-01-14 16:29:00 +01:00
Uruk
fdaa69a787 fix(i18n): missing typo and comma 2025-01-14 13:51:43 +01:00
sarendsen
ed5403e597 wip 2025-01-14 10:37:20 +01:00
sarendsen
e6f290b85f wip 2025-01-14 10:35:21 +01:00
sarendsen
aa20d9c701 wip 2025-01-14 10:31:16 +01:00
sarendsen
e7128afb32 wip 2025-01-14 09:48:17 +01:00
sarendsen
a24b126539 wip 2025-01-14 09:24:31 +01:00
sarendsen
e1fe20db86 wip 2025-01-14 07:56:32 +01:00
Simon Caron
cd9f6aa8bd update submodule 2025-01-14 00:06:14 -05:00
Simon Caron
747bd1b416 Merge branch 'develop' into feat/i18n 2025-01-13 22:35:05 -05:00
Simon Caron
364ce46fe5 Screen Orientation Enum + Subtitle Mode 2025-01-13 22:30:57 -05:00
Simon Caron
5703279b46 Merge develop 2025-01-13 21:18:37 -05:00
lostb1t
4022ccb213 feat: Custom homescreen support (#424) 2025-01-13 19:48:19 +01:00
Fredrik Burmester
3a836462f5 Merge pull request #422 from simoncaron/feat/hide-log-page-title
fix: Remove Page Path from Log Page Header
2025-01-13 17:58:39 +01:00
herrrta
8a5f24002f fix: unauthorized plugin access & null default values 2025-01-13 08:30:11 -05:00
retardgerman
c30f9860ee fix: fixed syntax errors 2025-01-13 12:31:23 +01:00
sarendsen
94c170e3d2 chore: some linting 2025-01-13 10:32:03 +01:00
Simon Caron
cd8aba32d8 Jellyseerr 2025-01-13 00:03:41 -05:00
Simon Caron
15f3ddf612 fix: Remove Page Path from Log Page 2025-01-12 23:00:12 -05:00
Simon Caron
90f20f6e46 Shorter messages 2025-01-12 21:34:08 -05:00
Simon Caron
ea1f45bbaf More settings + language component spacing 2025-01-12 21:30:57 -05:00
Simon Caron
7e62c9bc9a Merge branch 'develop' into feat/i18n 2025-01-12 19:49:58 -05:00
herrrta
23f9e9dfae fix: Override default settings with plugin unlocked default settings
- This sets the defaults on login and allows users to still change them
2025-01-12 19:24:01 -05:00
Simon Caron
580e12b605 Alert 2025-01-12 19:04:51 -05:00
Fredrik Burmester
ff4c5f28af chore 2025-01-12 14:11:09 +01:00
Fredrik Burmester
1b931ea348 Merge pull request #419 from streamyfin/fix/remove-music
fix: remove everything related to music
2025-01-12 14:07:59 +01:00
Fredrik Burmester
49c0437f81 fix: change opacity on press 2025-01-12 14:04:12 +01:00
Fredrik Burmester
d81ae94ce8 fix: add version to issue template 2025-01-12 13:41:33 +01:00
Fredrik Burmester
7c77c70024 chore: remove everything related to music 2025-01-12 13:40:01 +01:00
retardgerman
b28c4a56f3 fix: add new Releases to dropdown 2025-01-12 13:39:43 +01:00
Fredrik Burmester
2495a318eb Merge pull request #394 from Ryan0204/enhancement/autohidecontrol
enhancement: auto hide control after 5 seconds
2025-01-12 10:16:55 +01:00
Fredrik Burmester
7832ea4d0a chore: deps 2025-01-12 10:10:18 +01:00
Fredrik Burmester
4a0a51ef1d chore: refactor 2025-01-12 10:07:49 +01:00
Fredrik Burmester
8cc551d906 Merge pull request #416 from streamyfin/feat/server-discovery
feat: server discovery during login
2025-01-12 09:37:33 +01:00
Fredrik Burmester
c8da365a00 fix: issues listed in pr 2025-01-12 09:36:23 +01:00
Fredrik Burmester
74b7cbc530 Merge pull request #417 from whoopsi-daisy/patch-1
Update README.md
2025-01-12 09:33:38 +01:00
𝐂𝐡𝐫𝐢𝐬
a14063a736 Update README.md
Adjusted the Jellyseerr screenshot height to match the others and corrected a typo, along with rephrasing a sentence for clarity
2025-01-12 00:52:17 +08:00
Fredrik Burmester
a3307a90a3 feat: server discovery during login 2025-01-11 11:21:36 +01:00
Fredrik Burmester
a2145fd7e8 chore: update deps 2025-01-11 10:20:20 +01:00
Fredrik Burmester
cab5e4d980 chore: rename var 2025-01-11 10:10:00 +01:00
Fredrik Burmester
ab603e6997 feat: add centralised plugin info 2025-01-11 10:09:53 +01:00
ryan0204
957348fe19 prevent opening control when user swipe on screen 2025-01-11 16:41:41 +08:00
herrrta
444bd040b0 Merge pull request #402 from streamyfin/feat/401
Streamyfin Plugin App Management solution
2025-01-11 00:20:35 -05:00
herrrta
d0ae63235d feat: [StreamyfinPlugin] Library Options settings 2025-01-11 00:16:26 -05:00
herrrta
1727125ea7 feat: [StreamyfinPlugin] Popular Plugin settings 2025-01-11 00:16:25 -05:00
herrrta
dc498d62d8 feat: [StreamyfinPlugin] Other settings 2025-01-11 00:16:25 -05:00
herrrta
455bf08213 feat: [StreamyfinPlugin] Subtitle Toggles settings
- Used stepper & dropdown components to simplify page
2025-01-11 00:16:20 -05:00
herrrta
0f974ef2a3 feat: [StreamyfinPlugin] Audio Toggles settings 2025-01-10 23:26:53 -05:00
herrrta
2d9aaccfe0 feat: [StreamyfinPlugin] Media Toggles settings 2025-01-10 23:26:44 -05:00
herrrta
2c6823eb53 feat: [StreamyfinPlugin] Jellyseerr, Search Engine, & Download settings
- Added DisabledSetting.tsx component
- Added DownloadMethod enum
- cleanup
2025-01-10 23:26:32 -05:00
herrrta
9dfcc01f17 chore 2025-01-10 20:39:32 -05:00
Fredrik Burmester
38aad9610b Merge branch 'feat/401' of https://github.com/streamyfin/streamyfin into feat/401 2025-01-09 15:49:02 +01:00
herrrta
54af64abef api augmentations & added streamyfin plugin id 2025-01-09 09:35:24 -05:00
herrrta
e1720a00da initial changes 2025-01-09 09:33:55 -05:00
herrrta
882d0ea188 api augmentations & added streamyfin plugin id 2025-01-09 08:51:53 -05:00
Fredrik Burmester
f3b539232f Merge pull request #403 from streamyfin/feature/mediasourcenames
Feature: Remove duplicate names from media sources
2025-01-09 11:33:06 +01:00
sarendsen
33ea657a5c Filter out duplicate names in media sources 2025-01-09 10:13:57 +01:00
herrrta
75820adcbc initial changes 2025-01-08 21:52:31 -05:00
herrrta
76cdb2b3f8 fix cast npe 2025-01-08 17:31:39 -05:00
Fredrik Burmester
0a2ea33635 Merge pull request #397 from topiga/master 2025-01-08 18:50:02 +01:00
sarendsen
3cd8e41000 wip 2025-01-08 15:25:06 +01:00
Théo FORTIN
aad6093852 Added 1 Mb/s as bitrate 2025-01-08 15:22:11 +01:00
herrrta
c553cff9d1 Added clean script 2025-01-08 08:05:06 -05:00
herrrta
dcd458bd3d [Jellyseerr] "Currently Streaming On" misaligned text
fixes #392
2025-01-08 08:04:48 -05:00
Fredrik Burmester
05dc61d17d Merge pull request #395 from streamyfin/feat/331
[Jellyseerr] Show media configuration for admins
2025-01-08 11:40:07 +01:00
Fredrik Burmester
e4de11127f chore 2025-01-08 11:39:50 +01:00
herrrta
2dc49735f4 [Jellyseerr] Show media configuration for admins
implements #331
2025-01-07 23:53:10 -05:00
ryan0204
0ebacd4bd3 Auto hide control after 5 seconds 2025-01-08 11:29:49 +08:00
Simon Caron
14c8c1aaed Fix some missing fields 2025-01-07 22:26:09 -05:00
Simon Caron
2da774272d Merge branch 'develop' into feat/i18n 2025-01-07 20:38:59 -05:00
sarendsen
dd08826931 wip 2025-01-07 12:03:35 +01:00
sarendsen
b681025389 wip 2025-01-07 12:01:55 +01:00
Fredrik Burmester
ef42207174 Merge pull request #383 from Ryan0204/master
Change ScreenOrientation to landscape right by default and added toggleSafeArea for all videos
2025-01-07 11:01:43 +01:00
Fredrik Burmester
efa5638b12 fix: remove tab sidebar 2025-01-07 10:59:21 +01:00
sarendsen
65549428bf wip 2025-01-07 10:45:25 +01:00
Fredrik Burmester
c63cea891d chore: remove imports 2025-01-07 10:27:56 +01:00
Fredrik Burmester
4e80f58823 fix: backdrop not filling screen 2025-01-07 10:27:30 +01:00
sarendsen
cda3b64a2b wip 2025-01-07 10:08:07 +01:00
ryan0204
cfe39d504c Rotate ScreenOrientation back on exit player 2025-01-07 14:13:22 +08:00
herrrta
cf43d1a657 cleanup 2025-01-06 20:56:59 -05:00
herrrta
cbe3b18226 fix enter animation 2025-01-06 20:55:50 -05:00
herrrta
b637a0f7d2 use JellyseerrPoster component 2025-01-06 20:55:48 -05:00
Fredrik Burmester
a0ce7cc6d0 chore 2025-01-06 22:58:11 +01:00
Fredrik Burmester
a640df30bc chore 2025-01-06 22:32:27 +01:00
Fredrik Burmester
062e6e6c23 chore 2025-01-06 22:21:27 +01:00
Fredrik Burmester
d709e3b13e Merge pull request #389 from streamyfin/feat/326
[Jellyseerr] Show genre/studio/network discover sliders
2025-01-06 20:57:10 +01:00
herrrta
b232bebd73 [Jellyseerr] Show genre/studio/network discover sliders
implements #326
2025-01-06 14:25:14 -05:00
Fredrik Burmester
90ef8ef6f9 feat: fade in images 2025-01-06 17:38:59 +01:00
Fredrik Burmester
0df6b8e2a0 chore 2025-01-06 17:33:49 +01:00
Fredrik Burmester
f48b26076d feat: loading skeleton for search (including jellyseerr) 2025-01-06 17:33:27 +01:00
ryan0204
c86a8438e5 Bring back toggleSafeArea button for all videos 2025-01-06 23:15:00 +08:00
sarendsen
373d4ca3b1 wip 2025-01-06 15:10:59 +01:00
sarendsen
8bc360d554 wip 2025-01-06 15:04:07 +01:00
sarendsen
3fae21d559 wip 2025-01-06 14:45:42 +01:00
sarendsen
74ce9d7eea wip 2025-01-06 14:28:24 +01:00
sarendsen
5055a700c9 wip 2025-01-06 13:59:56 +01:00
Ryan
faa2baae68 Merge branch 'streamyfin:master' into master 2025-01-06 20:28:12 +08:00
ryan0204
ed42371353 Change ScreenOrientation to landscape right by default 2025-01-06 20:27:34 +08:00
sarendsen
ab33693dd9 wip 2025-01-06 13:25:49 +01:00
Fredrik Burmester
24277135a8 Merge branch 'master' into develop 2025-01-06 10:14:27 +01:00
Fredrik Burmester
23d9cd36d1 chore 2025-01-06 10:14:17 +01:00
Fredrik Burmester
b243524a7d chore 2025-01-06 10:13:40 +01:00
Fredrik Burmester
8288682e68 feat: intro screen 2025-01-05 23:28:24 +01:00
Fredrik Burmester
58ec915699 feat: intro page 2025-01-05 22:17:02 +01:00
Simon Caron
480abb216d fixes 2025-01-05 16:07:55 -05:00
Simon Caron
249109a94e livetv 2025-01-05 16:03:19 -05:00
Simon Caron
eb7fa93f9b remove dupe 2025-01-05 15:26:48 -05:00
Simon Caron
e8fd322d30 Merge branch 'master' into feat/i18n 2025-01-05 15:06:44 -05:00
Fredrik Burmester
cad03a3566 fix: chromecast 2025-01-05 20:59:45 +01:00
Fredrik Burmester
9baa4063bd chore 2025-01-05 16:22:52 +01:00
Fredrik Burmester
41db34ed8e fix: hide libraries from home sections as well 2025-01-05 16:22:32 +01:00
retardgerman
5aba66ce05 fix: add jellyseerr screenshot 2025-01-05 16:09:06 +01:00
retardgerman
79407ccd70 Add files via upload 2025-01-05 16:06:21 +01:00
retardgerman
9a93b3b3bb Delete jellyseerr.PNG 2025-01-05 16:05:51 +01:00
retardgerman
2b846a1aca Add files via upload 2025-01-05 16:01:02 +01:00
Fredrik Burmester
55d61172f4 Merge branch 'master' into develop 2025-01-05 15:53:06 +01:00
Fredrik Burmester
57173a62dc Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-01-05 15:51:52 +01:00
Fredrik Burmester
78f65be09d Merge branch 'master' into develop 2025-01-05 15:51:33 +01:00
Fredrik Burmester
293a9517a5 Merge pull request #377 from anubhavvs/feature/haptic-feedback-toggle
feat: haptic feedback settings and custom hook
2025-01-05 15:50:57 +01:00
Fredrik Burmester
38b6215046 Merge pull request #376 from eltociear/patch-1
chore: update useDefaultPlaySettings.ts
2025-01-05 15:48:32 +01:00
Fredrik Burmester
fc4a11d916 Merge pull request #378 from streamyfin/feat/hide-libraries
feat: hide libraries from libraries tab
2025-01-05 15:47:29 +01:00
Fredrik Burmester
cf2beb8299 feat: hide libraries 2025-01-05 15:46:44 +01:00
Fredrik Burmester
49d157a95a fix: overlapping buttons with navbar fixes #373 2025-01-05 15:29:39 +01:00
Anubhav Saha
9692c173ae feat: haptic feedback settings and custom hook 2025-01-05 15:22:52 +01:00
Ikko Eltociear Ashimine
a297ac4843 chore: update useDefaultPlaySettings.ts
intial -> initial
2025-01-05 23:08:44 +09:00
Fredrik Burmester
6a4621c377 Merge branch 'develop' into feature/bigscreen 2025-01-05 13:43:23 +01:00
Fredrik Burmester
a061f9f480 fix: padding 2025-01-05 12:04:11 +01:00
Fredrik Burmester
0fb6f2fb30 fix: padding 2025-01-05 12:04:07 +01:00
Fredrik Burmester
0773f773ba fix: padding 2025-01-05 12:04:02 +01:00
Fredrik Burmester
39bb3a9370 fix: padding 2025-01-05 12:03:56 +01:00
Fredrik Burmester
b79e534692 Merge branch 'develop' of https://github.com/streamyfin/streamyfin into develop 2025-01-05 11:56:53 +01:00
Fredrik Burmester
e9336e9a67 feat: new useQuery specifically for react navigation to invalidate !enabled queries on screen re-mount 2025-01-05 11:56:46 +01:00
Fredrik Burmester
adfde1a7cd fix: update request status in poster on search scree 2025-01-05 11:56:16 +01:00
Fredrik Burmester
cab6257fb2 feat: hook for request permissions 2025-01-05 11:55:52 +01:00
Fredrik Burmester
3f0f0090af fix: refactor permissions 2025-01-05 11:55:41 +01:00
Fredrik Burmester
b278632581 fix: height on loading button 2025-01-05 11:55:29 +01:00
Fredrik Burmester
5ee1a9cabb fix: change keys 2025-01-05 11:55:22 +01:00
Fredrik Burmester
2169bea031 fix: add loading and refactor permissions 2025-01-05 11:55:05 +01:00
sarendsen
2fb19f601b remove reload 2025-01-05 10:54:14 +01:00
sarendsen
a602c35a8f refactor: Add support for tvos 2025-01-05 10:43:10 +01:00
retardgerman
46ac4a2cc7 fix: auto add feature requests to roadmap 2025-01-05 10:42:08 +01:00
retardgerman
962f65874e fix: removed assignees and modified link to roadmap 2025-01-05 10:42:08 +01:00
Fredrik Burmester
95cf252349 Delete svenska_kyrkan.sql 2025-01-05 10:29:59 +01:00
Fredrik Burmester
8470cbe8d5 Delete svenska_kyrkan.sql 2025-01-05 10:29:12 +01:00
Fredrik Burmester
636a27246f chore: deps 2025-01-05 09:51:35 +01:00
Fredrik Burmester
a488c68633 fix: sizing 2025-01-05 09:49:57 +01:00
Fredrik Burmester
7342b7eb92 chore: deps 2025-01-05 09:46:51 +01:00
Fredrik Burmester
8370519758 chore: formatting 2025-01-05 09:46:48 +01:00
Fredrik Burmester
85e21edbf1 fix: padding in person view appearences grid 2025-01-05 09:46:30 +01:00
Fredrik Burmester
8d4115f5a0 fix: app crash on internal yt trailer, link out instead 2025-01-05 09:24:54 +01:00
herrrta
c5d7a6729b Merge pull request #372 from herrrta/feat/327
[Jellyseerr] Add cast/crew results
2025-01-05 02:55:41 -05:00
herrrta
db4046267f [Jellyseerr] Add cast/crew results
implements #327
2025-01-05 02:53:41 -05:00
retardgerman
1e869a2c2f fix: auto add feature requests to roadmap 2025-01-05 08:28:32 +01:00
retardgerman
b6502c042a fix: removed assignees and modified link to roadmap 2025-01-05 08:16:04 +01:00
herrrta
b506871c46 Merge pull request #371 from herrrta/feat/328
[Jellyseerr] Add external links to trailers
2025-01-04 21:29:10 -05:00
herrrta
734678b1d5 [Jellyseerr] Add external links to trailers
implements #328
2025-01-04 21:28:45 -05:00
herrrta
68e98bbb94 Merge pull request #370 from herrrta/feat/325
[Jellyseerr] Show facts about media result
2025-01-04 21:06:32 -05:00
herrrta
d84ed558f3 [Jellyseerr] Show facts about media result
implements #325
2025-01-04 21:04:12 -05:00
herrrta
ad39e8e10a Merge pull request #369 from herrrta/fix/lock-ios-util-version
Fix/lock ios util version
2025-01-04 17:25:09 -05:00
herrrta
29bba04fdd react-native-ios-utilities major having issues
locked to 4.5.1
2025-01-04 17:24:52 -05:00
herrrta
5a24957e88 Merge pull request #368 from herrrta/fix/332
Add badge to differentiate a movie/series
2025-01-04 17:24:30 -05:00
herrrta
39f2735756 Add badge to differentiate a movie/series
fixes #332
2025-01-04 17:11:01 -05:00
Simon Caron
53ea1cc899 More Translations 2025-01-04 16:41:54 -05:00
Fredrik Burmester
5dc86d4765 Merge pull request #365 from herrrta/fix/363
Requesting using purple request button doesnt refresh page
2025-01-04 22:00:13 +01:00
herrrta
d13731c28f Requesting using purple request button doesnt refresh page
fixes #363
2025-01-04 15:51:58 -05:00
Simon Caron
459ca3245b Rename card field 2025-01-04 15:39:04 -05:00
Simon Caron
0d1fb87284 Fix Language Selector Setting Component 2025-01-04 15:26:24 -05:00
Fredrik Burmester
7f0446b85f Update build-ios.yaml 2025-01-04 21:13:57 +01:00
Fredrik Burmester
11fbe19f80 Update build-ios.yaml 2025-01-04 20:57:57 +01:00
Simon Caron
495742c52c Merge branch 'master' into feat/i18n 2025-01-04 14:57:45 -05:00
Fredrik Burmester
5c97b85492 Update build-ios.yaml 2025-01-04 20:50:09 +01:00
Simon Caron
894305e126 Item Card Fields 2025-01-04 14:49:56 -05:00
Fredrik Burmester
e60cec69f8 Update build-ios.yaml 2025-01-04 20:49:12 +01:00
Fredrik Burmester
7bc1c22770 Merge pull request #355 from Stetsed/master
Implement CI/CD pipeline for iOS building
2025-01-04 20:30:12 +01:00
Stetsed
e86dab5613 Update build-ios.yaml 2025-01-04 19:15:09 +01:00
Stetsed
eeb803223c Allow manual build trigger 2025-01-04 19:04:29 +01:00
Stetsed
1a43f7ef1b Create build-ios.yaml 2025-01-04 19:03:12 +01:00
retardgerman
f4624bdc25 fix: typo 2025-01-04 16:32:46 +01:00
Fredrik Burmester
3c5f2b4079 fix: controls gray overlay 2025-01-04 13:04:23 +01:00
Fredrik Burmester
955190a9cc chore: version 2025-01-04 13:04:15 +01:00
Fredrik Burmester
e1e4f4833c chore: versions 2025-01-04 13:04:02 +01:00
Fredrik Burmester
3b987646a6 fix: adjust to working version 2025-01-03 23:27:52 +01:00
Simon Caron
ed993d07ce Types 2025-01-03 16:33:51 -05:00
Fredrik Burmester
0e574ea18d fix: alert prompt not working on android 2025-01-03 21:43:07 +01:00
Simon Caron
dc9008f31c Merge branch 'master' into feat/i18n 2025-01-03 15:23:17 -05:00
Fredrik Burmester
1a5fcdcb10 chore 2025-01-03 21:15:33 +01:00
Fredrik Burmester
62b00837ec fix: ? 2025-01-03 10:26:12 +01:00
Fredrik Burmester
0fc48497d0 fix: unclear phrasing 2025-01-03 10:24:16 +01:00
Fredrik Burmester
7e12136211 fix: missing text 2025-01-03 10:23:05 +01:00
Fredrik Burmester
7639de153b chore 2025-01-03 10:12:43 +01:00
Fredrik Burmester
ea3cc18b3c fix: item count 2025-01-03 10:12:40 +01:00
Fredrik Burmester
c9fb52086e fix: key warning 2025-01-03 09:55:48 +01:00
Fredrik Burmester
878edc6909 fix: spelling 2025-01-02 22:15:09 +01:00
Fredrik Burmester
74f0aca517 fix: text 2025-01-02 22:08:47 +01:00
Fredrik Burmester
60bb3b905d feat: save previous servers 2025-01-02 21:59:05 +01:00
Fredrik Burmester
fdde5fb56c Merge pull request #350 from herrrta/fix/334
Refresh jellyseerr page when media is requested
2025-01-02 20:49:04 +01:00
Fredrik Burmester
49ae9c6f57 chore 2025-01-02 20:48:44 +01:00
Fredrik Burmester
2254adb8d6 Merge branch 'pr/350' 2025-01-02 20:47:55 +01:00
herrrta
d4c722aeac Refresh jellyseerr page when media is requested
- refetch details when media request is successful
- fix key error on tags
2025-01-02 14:47:36 -05:00
Fredrik Burmester
eefcfb8be5 fix: plugins design stuff 2025-01-02 20:46:34 +01:00
herrrta
4af2712cc0 Refresh jellyseerr page when media is requested
- refetch details when media request is successful
- fix key error on tags
- chore: update bun.lockb
2025-01-02 14:08:15 -05:00
Fredrik Burmester
958b870bf0 Merge pull request #349 from streamyfin/refactor/settings-2
feat: refactor settings
2025-01-02 17:33:42 +01:00
Fredrik Burmester
ce7e1b255f fix: design 2025-01-02 17:32:50 +01:00
Fredrik Burmester
acae4b4544 fix: update deps 2025-01-02 17:32:45 +01:00
Fredrik Burmester
f7bbb20c38 fix: popular lists info 2025-01-02 17:32:29 +01:00
Fredrik Burmester
2c655b9482 chore 2025-01-02 17:25:03 +01:00
Fredrik Burmester
b8dbce6bf2 fix: popular lists plugin 2025-01-02 17:24:43 +01:00
Fredrik Burmester
730823c520 fix: marlin search settings 2025-01-02 17:18:01 +01:00
Fredrik Burmester
77f14a7d5b fix: spelling 2025-01-02 16:50:58 +01:00
Fredrik Burmester
07c7cb7ab5 feat: refactor settings 2025-01-02 16:49:04 +01:00
Fredrik Burmester
5333d53d61 fix: chromecast on android 2025-01-02 12:24:25 +01:00
Fredrik Burmester
82e50b9ba3 fix: crash if item not properly analuzed serverside 2025-01-02 12:24:17 +01:00
Fredrik Burmester
663605b9e8 chore 2025-01-02 12:23:58 +01:00
Fredrik Burmester
00847c8d3d fix: correct routing for actors after favorite tab implementation 2025-01-02 11:45:17 +01:00
retardgerman
f20ad67186 fix: updated links 2025-01-02 08:06:30 +01:00
Simon Caron
e23387a384 Library headers, filters and favorites 2025-01-01 21:57:46 -05:00
Simon Caron
bb141cad57 Merge branch 'master' into feat/i18n 2025-01-01 21:32:24 -05:00
Simon Caron
e833b4bc68 Alert and Toasts 2025-01-01 21:31:04 -05:00
Simon Caron
34fc26ed18 Quick connect alerts 2025-01-01 20:29:39 -05:00
Fredrik Burmester
91527b83dd Merge pull request #347 from herrrta/fix/333
Make report issue bottom sheet full screen
2025-01-01 20:18:52 +01:00
herrrta
14138151a3 Make report issue bottom sheet full screen
- Used BottomSheetTextInput for keyboard focus + blur behaviors
2025-01-01 14:04:50 -05:00
Fredrik Burmester
6c2bfe2a45 fix: spacing 2025-01-01 18:33:03 +01:00
Fredrik Burmester
996cd36a9e fix: invalidate cache on favorite 2025-01-01 18:30:16 +01:00
Fredrik Burmester
6aa2e00d93 fix: wrong format 2025-01-01 18:26:53 +01:00
Fredrik Burmester
344e0932dc fix: add favorite to series 2025-01-01 18:23:42 +01:00
Fredrik Burmester
eaffffb2f0 Merge branch 'master' of https://github.com/streamyfin/streamyfin 2025-01-01 18:11:20 +01:00
Fredrik Burmester
f6c0513d2d feat: favorite button toggle for movies/episodes 2025-01-01 18:11:18 +01:00
Fredrik Burmester
013f064280 Merge pull request #346 from sldless/master
Github Workflow
2025-01-01 17:54:26 +01:00
sldless
cd2c3f359e Revert "feat: add favorites functionality with FavoriteButton component and layout"
This reverts commit 468f58e531.
2025-01-01 11:47:43 -05:00
sldless
123c6bba05 Revert "Delete bun.lockb"
This reverts commit 347f196a6a.
2025-01-01 11:47:37 -05:00
sldless
a1ea926342 feat: add Discord notification workflow for new pull requests 2025-01-01 11:28:36 -05:00
Fredrik Burmester
6a17ac02af fix: horrizontal padding issue 2025-01-01 17:12:38 +01:00
Fredrik Burmester
815be2a175 Merge pull request #345 from streamyfin/feat/favorites-tab
feat: favorites tab
2025-01-01 16:23:31 +01:00
Fredrik Burmester
ece3bc001f fix: id collision 2025-01-01 16:11:04 +01:00
Fredrik Burmester
27609e7789 feat: favorites tab 2025-01-01 15:46:22 +01:00
Fredrik Burmester
40b8410390 feat: enable manually setting language in settings 2025-01-01 11:25:02 +01:00
sldless
347f196a6a Delete bun.lockb 2024-12-31 22:35:00 -05:00
sldless
468f58e531 feat: add favorites functionality with FavoriteButton component and layout 2024-12-31 22:29:09 -05:00
Fredrik Burmester
a994868be4 chore 2024-12-31 23:13:58 +01:00
Simon Caron
723233381c Settings Fields V 2024-12-31 16:09:12 -05:00
Fredrik Burmester
ee6d43e3e8 Merge branch 'master' of https://github.com/streamyfin/streamyfin 2024-12-31 21:47:46 +01:00
Fredrik Burmester
f8d22bb7d6 fix: trailers for movies 2024-12-31 21:47:43 +01:00
Simon Caron
602de34824 Settings fields 2024-12-31 15:31:36 -05:00
Simon Caron
9b1f2a98e5 Update translation key casing to snake_case 2024-12-31 14:43:40 -05:00
Simon Caron
946de97580 Remove LanguageSwitcher 2024-12-31 14:39:04 -05:00
Simon Caron
f2eadabf6a bump libs versions 2024-12-31 13:52:58 -05:00
Simon Caron
373d83a0d5 Basic downloads stack translation 2024-12-31 13:34:32 -05:00
Simon Caron
2c0ba18b49 Clean up const declarations 2024-12-31 13:10:46 -05:00
Simon Caron
3e8e8e1163 Merge branch 'master' into feat/i18n 2024-12-31 12:24:28 -05:00
Fredrik Burmester
a0391b484d Merge pull request #338 from streamyfin/retardgerman-patch-1
feat: explaining new way to enter Beta in README
2024-12-31 17:25:05 +01:00
retardgerman
681aadb121 feat: explaining new way to enter Beta in README 2024-12-31 17:24:09 +01:00
Fredrik Burmester
479a1f037e fix: padding 2024-12-31 16:07:16 +01:00
Fredrik Burmester
ae5b88ab56 feat: series info and trailer 2024-12-31 16:01:30 +01:00
Fredrik Burmester
9091b9b66a fix: poster not clickable 2024-12-31 16:01:19 +01:00
Fredrik Burmester
cccb26c9cc chore 2024-12-31 15:00:30 +01:00
Fredrik Burmester
28568cbb9c fix: incorrect logic 2024-12-31 14:56:41 +01:00
Fredrik Burmester
8344d4025b fix: not possible to select seasons without index 2024-12-31 14:54:31 +01:00
Fredrik Burmester
0f69448081 fix: design 2024-12-31 13:40:23 +01:00
Fredrik Burmester
a936916da4 fix: design 2024-12-31 13:35:59 +01:00
Fredrik Burmester
c753e33f38 chore 2024-12-31 13:32:51 +01:00
Fredrik Burmester
48422fa93e fix: design 2024-12-31 13:32:43 +01:00
Fredrik Burmester
5adf943fd9 fix: finally fix not rotten tomatoes score 2024-12-31 13:05:23 +01:00
Fredrik Burmester
9174a8104d fix: color 2024-12-31 10:55:23 +01:00
Fredrik Burmester
56f1bd489c fix: improve readme 2024-12-31 10:35:16 +01:00
Fredrik Burmester
5e79b5a581 fix: improve readme 2024-12-31 10:33:59 +01:00
Fredrik Burmester
36a689f59d fix: improved login flow for jellyseerr 2024-12-31 10:29:50 +01:00
Simon Caron
fe9c73a8f0 Library Translation 2024-12-30 21:52:34 -05:00
Simon Caron
4f62391027 Add fr, search translation, fix login title 2024-12-30 21:38:42 -05:00
Simon Caron
53b5fdda87 fix import 2024-12-30 21:13:52 -05:00
Simon Caron
c0b71eb73d Revert login message 2024-12-30 21:03:02 -05:00
Simon Caron
9b4590c876 Update Current Translated Messages with UI Changes 2024-12-30 20:06:56 -05:00
herrrta
47211ba009 Merge pull request #323 from herrrta/fix/movie-request-crash
Fix movie request crashing
2024-12-30 16:53:03 -05:00
herrrta
e86a2af9a9 Fix movie request crashing 2024-12-30 16:52:44 -05:00
Simon Caron
4b18bad3bc Merge branch 'master' into feat/i18n 2024-12-30 16:45:41 -05:00
herrrta
c46b4cc34d Merge pull request #322 from herrrta/fix/discover-spacing
Fix spacing between slides on discover page
2024-12-30 16:34:40 -05:00
herrrta
ec0d9d7788 Fix spacing between slides on discover page 2024-12-30 16:34:15 -05:00
Fredrik Burmester
d2eda1365c Merge pull request #321 from herrrta/fix/jellyseerr-pass-prompt
Better jellyseerr password input with loading indicator
2024-12-30 22:02:09 +01:00
herrrta
b58fa86a6b Better jellyseerr password input with loading indicator 2024-12-30 16:00:04 -05:00
Fredrik Burmester
400dfe3679 Merge pull request #320 from herrrta/fix/jellyseerr-cookies
Some jellyseerr clients dont set cookies
2024-12-30 15:17:50 +01:00
herrrta
cf58a5e749 Some jellyseerr clients dont set cookies 2024-12-30 09:12:00 -05:00
Fredrik Burmester
001eba02b4 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-12-30 13:45:15 +01:00
Fredrik Burmester
67e767f298 chore: versions 2024-12-30 13:44:28 +01:00
Fredrik Burmester
5f1c5f7b34 Merge pull request #317 from herrrta/feat/jellyseerr-discover
Basic Jellyseerr discover page
2024-12-30 11:42:29 +01:00
Fredrik Burmester
e54cac1e09 Merge pull request #315 from herrrta/fix/more-jellyseerr-logs
More logs on failed jellyseerr status test
2024-12-30 11:41:38 +01:00
herrrta
cbce83e109 Basic Jellyseerr discover page 2024-12-29 13:50:02 -05:00
herrrta
c6b58c5c28 More logs on failed jellyseerr status test 2024-12-28 12:20:49 -05:00
Fredrik Burmester
0468756317 Merge pull request #309 from herrrta/jellyseer-integration
Jellyseerr Integration
2024-12-28 13:37:20 +01:00
herrrta
9f12ee027f Jellyseerr Integration
## Note this is early stages of said integration. Things will change!
series and season working

- added jellyseerr git submodule
- augmentations
- working jellyseerr search integration
- working jellyseerr requests & updated interceptors to persist cookies from every response
2024-12-27 22:20:33 -05:00
Fredrik Burmester
78b7425c6b Merge pull request #311 from lostb1t/fix/mergedversions
Fix merged versions showing as separate media in listings.
2024-12-26 11:44:03 +01:00
Fredrik Burmester
c38c1d06ad Merge pull request #307 from Alexk2309/hotfix/dropdown-spacing
Added padding and spacing for dropdown button
2024-12-26 11:43:44 +01:00
sarendsen
5af735065a Remove console.log 2024-12-26 11:31:37 +01:00
sarendsen
600276cb69 Use recursive param so that merged versions show up as a single entity 2024-12-26 11:27:26 +01:00
Alex Kim
ba3104f87e Added padding and spacing for dropdown button 2024-12-26 11:07:33 +11:00
Fredrik Burmester
3aef9458e3 Merge pull request #303 from fredrikburmester/feat/context-menu-action-for-items
feat: context menu actions for items
2024-12-23 12:02:09 +01:00
Fredrik Burmester
5bce394836 Merge pull request #304 from fredrikburmester/feat/safe-areas-in-controls
feat: setting toggle for safe areas in controls
2024-12-23 12:01:31 +01:00
Fredrik Burmester
90930d478c fix: call with correct args 2024-12-23 11:25:24 +01:00
Fredrik Burmester
dd09f3d4d9 feat: settings toggle for safe areas in controls 2024-12-23 10:26:15 +01:00
Fredrik Burmester
8608ad02f7 feat: context menu actions for items 2024-12-22 11:57:44 +01:00
Fredrik Burmester
030947fc38 fix: login design 2024-12-22 11:33:15 +01:00
Fredrik Burmester
9b18188b32 fix: live tv design 2024-12-22 11:28:28 +01:00
Fredrik Burmester
d86853dec9 fix: login design 2024-12-22 11:28:21 +01:00
Fredrik Burmester
0750acdc13 Merge pull request #302 from Alexk2309/fix/remove-episode-list-and-next-up-in-offline-playback
Removed episodelist button and previous/next buttons for offline playback
2024-12-22 10:40:08 +01:00
Alex Kim
d8231f5b80 Fixed compile error 2024-12-22 17:46:56 +11:00
Alex Kim
41d17499bb Removed episodelist button and previous/next buttons for offline playback 2024-12-22 17:43:58 +11:00
Fredrik Burmester
60f1217cae Merge pull request #300 from herrrta/fix/storage-read-crash
dont rely on cache for downloadedItems
2024-12-21 22:02:49 +01:00
herrrta
834de10e34 dont rely on cache for downloadedItems 2024-12-21 14:10:55 -05:00
Fredrik Burmester
51f17f983d fix: add safe areas back to controls 2024-12-21 13:18:56 +01:00
Fredrik Burmester
ba4a2c0b79 fix: haptics 2024-12-21 13:03:32 +01:00
Fredrik Burmester
a32eb710ec feat: haptics 2024-12-21 12:56:04 +01:00
Fredrik Burmester
cb05da782a feat: optimistic update 2024-12-21 12:55:58 +01:00
Fredrik Burmester
5a680a4392 fix: smoother item page loading 2024-12-21 12:45:32 +01:00
Fredrik Burmester
8a44d2ff15 fix: correct types 2024-12-21 12:31:00 +01:00
Fredrik Burmester
f3f260625f fix: refactor buttons 2024-12-21 12:22:53 +01:00
Fredrik Burmester
6908620f4e fix: downloaded movie title overflowing 2024-12-19 15:10:21 +01:00
Fredrik Burmester
9932266203 fix: key error 2024-12-19 15:06:26 +01:00
Fredrik Burmester
cb2268e39c fix: don't show empty folders in movie/tvshow libraries 2024-12-19 15:05:31 +01:00
Fredrik Burmester
bf9be278d3 fix: texts and icons 2024-12-19 14:48:51 +01:00
Fredrik Burmester
584fcc09d6 fix: use ionicons 2024-12-19 14:18:53 +01:00
Fredrik Burmester
7a26b5004b fix: text reorder 2024-12-19 14:18:47 +01:00
Fredrik Burmester
ae92692ea0 fix: title props 2024-12-19 14:18:37 +01:00
Fredrik Burmester
92e4b3b8cf fix: use ionicons 2024-12-19 14:18:26 +01:00
Fredrik Burmester
127ec1391b fix: text 2024-12-19 14:12:29 +01:00
Fredrik Burmester
0ac4f826bc Merge pull request #297 from fredrikburmester/feat/technical-details
feat: add technical details to item
2024-12-19 12:06:12 +01:00
Fredrik Burmester
6190f2e602 feat: add technical details to item 2024-12-19 12:05:57 +01:00
Fredrik Burmester
24fdd071af Merge pull request #294 from jakequade/master
fix: restore streaming codecs
2024-12-19 10:11:04 +01:00
jakequade
be3122caac add in proper codecs for chromecast 2024-12-17 20:41:07 +11:00
Fredrik Burmester
39a220bbed Merge pull request #289 from herrrta/fix/delete-type
Move downloads to a cache directory
2024-12-14 22:49:15 +01:00
herrrta
e3bdbb5cbd Move downloads to a cache directory
- cleanup cache during apps first cold start
- Downloads now saved in cacheDirectory and moved to documents when verified complete
- Bring back download size to episode card
- Improve reading a files size if its a known downloaded file
- Added decimal to divisor in bytesToReadable for more accurate file size conversions
2024-12-14 12:19:44 -05:00
Fredrik Burmester
b6ad05d980 Merge pull request #284 from Alexk2309/hotfix/transcoded-streams
Hotfix/transcoded streams
2024-12-13 07:48:54 +01:00
Alex Kim
0360b5cbd5 Merged changes from main 2024-12-13 16:39:58 +11:00
Alex Kim
a9b1d9fb0a Added bandaid fix 2024-12-13 05:03:16 +11:00
Alex Kim
4291ef55b9 Added tmp fix 2024-12-13 01:04:55 +11:00
Fredrik Burmester
655060fb40 Merge pull request #282 from Alexk2309/hotfix/for-settings
Hotfix/for settings
2024-12-12 12:20:35 +01:00
Alex Kim
0e29b8b671 Added temporary fix 2024-12-12 21:41:22 +11:00
Alex Kim
72f64c71dd Added .vscode to git ignore 2024-12-12 16:37:06 +11:00
Alex Kim
ddfd9f6ce3 Added vscode styling for pretier extension 2024-12-12 16:36:17 +11:00
Alex Kim
67fb339d40 Added fix that fully stops the UseEffect hook from been calling indefinetly 2024-12-12 16:33:30 +11:00
Alex Kim
9e0a7f047c Added new pulled state, to stop infinite callback for useEffect hookt in MediaContext 2024-12-12 15:44:53 +11:00
Fredrik Burmester
aab806bbf4 Merge pull request #281 from Alexk2309/hotfix/fix-options-after-subtitle-audio-revamp
Added fix
2024-12-11 19:39:37 +01:00
Alex Kim
4a53b20618 Added fix 2024-12-12 05:38:14 +11:00
Fredrik Burmester
45299a5c5d Merge pull request #279 from Alexk2309/revamp/audio-subtitle-selection
Revamp/audio subtitle selection
2024-12-11 18:59:24 +01:00
Alex Kim
65ad4effca Merged main into branch 2024-12-12 04:25:37 +11:00
Alex Kim
35fcb5ca0c Completed subtitle feature 2024-12-12 04:23:09 +11:00
Fredrik Burmester
5dc0066370 fix: remove auto http/s and allow for more flexible urls 2024-12-11 18:02:27 +01:00
Alex Kim
3fb20a8ca2 Revamped transcoding subtitles 2024-12-12 02:41:30 +11:00
Fredrik Burmester
180ed54fed fix: initial support playlists 2024-12-11 15:49:20 +01:00
Fredrik Burmester
72859b4ae3 fix: make the next episode button work with skip credits button 2024-12-11 08:28:11 +01:00
Fredrik Burmester
bfe96edb29 fix: don't show if no next episode 2024-12-10 21:52:46 +01:00
Fredrik Burmester
46f4acdad0 Merge pull request #276 from fredrikburmester/feat/go-to-next-episode-countdown
feat: go to next episode countdown
2024-12-10 21:11:51 +01:00
Fredrik Burmester
da1aa9f48c feat: go to next episode countdown 2024-12-10 20:37:58 +01:00
Alex Kim
1d0d99c79b Added stream ranker class 2024-12-11 06:02:13 +11:00
Alex Kim
33a6295b20 Added more selection options 2024-12-11 05:22:56 +11:00
Alex Kim
72cc381087 Added use default audio 2024-12-11 04:59:33 +11:00
Alex Kim
c4bfaf2d56 Made subtitle mode fetch from server 2024-12-11 04:52:12 +11:00
Alex Kim
487ac398e5 Added subtitle mode in options 2024-12-11 04:48:53 +11:00
Alex Kim
84fd0edc49 WIP 2024-12-11 04:01:30 +11:00
Fredrik Burmester
0e1583c440 Merge pull request #275 from Alexk2309/fix/select-same-bitrate-when-changing-episode-in-player
Fix for the selecting the same bitrate when changing the same episode…
2024-12-10 10:51:30 +01:00
Alex Kim
6459e5f323 Fix for the selecting the same bitrate when changing the same episode in the player 2024-12-10 20:48:15 +11:00
Fredrik Burmester
319e1fd53f Merge pull request #274 from Alexk2309/fix/trick-play-invalid-for-transcoded-player
Fix/trick play invalid for transcoded player
2024-12-10 10:24:39 +01:00
Alex Kim
93bd817eaf Removed old function argumement 2024-12-10 20:22:41 +11:00
Alex Kim
d9f21e6824 Removed Unused imports for controls 2024-12-10 20:21:53 +11:00
Alex Kim
d287f5d082 Added fix for invalid trickplay, for transcoded player 2024-12-10 20:21:00 +11:00
Fredrik Burmester
ecd2fa386e chore 2024-12-09 21:33:10 +01:00
Fredrik Burmester
7c022bbaff Merge pull request #273 from Alexk2309/hotfix/show-audio-slider-when-changing-audio-through-device
Added feature to show audio slider when changing audio through device
2024-12-09 21:32:42 +01:00
Alex Kim
5d79ee34cf Added feature to show audio slider when changing audio through device 2024-12-10 05:20:49 +11:00
Fredrik Burmester
b0adad8dc4 Merge pull request #272 from Alexk2309/feature/audio-slider-in-controls
Feature/audio slider in controls
2024-12-09 17:22:14 +01:00
Alex Kim
c3d3f538d7 Finished changes for audio selection 2024-12-10 03:21:02 +11:00
Alex Kim
6b6dedf303 WIP 2024-12-10 02:50:03 +11:00
Alex Kim
8d22e4c075 Added audioSlider.tsx 2024-12-10 02:20:52 +11:00
Fredrik Burmester
4dff26e8c3 Merge pull request #270 from Alexk2309/change/default-subtitle-size-for-different-platforms
Changed default subtitle size depending platform
2024-12-09 15:04:30 +01:00
Alex Kim
ee2edda507 Changed default subtitle size depending platform 2024-12-10 01:03:09 +11:00
Fredrik Burmester
9e6a8424db Merge pull request #269 from Alexk2309/fix/next-up-episodes-not-showing-for-some-series
Fix for next-up not showing up for some episodes of a series.
2024-12-09 15:02:11 +01:00
Alex Kim
d37ecc1bef Added fix for change 2024-12-10 00:51:47 +11:00
Fredrik Burmester
e70fd3ee45 Merge pull request #268 from Alexk2309/fix/default-subtitles-not-showing
Fix default subtitles not working on app.
2024-12-09 08:55:56 +01:00
Alex Kim
16e93513e2 Fixed issue 2024-12-09 05:51:04 +11:00
Fredrik Burmester
b0c506f85d Merge pull request #267 from Alexk2309/hotfix/small-ui-changes
Changed trickplay debounce to 10ms and added padding for EpisodeList
2024-12-08 19:22:15 +01:00
Alex Kim
b762aff6e2 Changed trickplay debounce to 10ms and added padding for EpisodeList 2024-12-09 04:46:09 +11:00
Fredrik Burmester
75639c4424 Merge pull request #266 from Alexk2309/hotfix/bug-fixes-for-player
Hotfix/bug fixes for player
2024-12-08 18:14:17 +01:00
Fredrik Burmester
4606ce1834 chore: update deps 2024-12-08 18:13:57 +01:00
Alex Kim
44bde8f41e Fixed more bugs 2024-12-09 04:12:13 +11:00
Alex Kim
828edad749 Added padding on right side only 2024-12-09 04:07:56 +11:00
Alex Kim
f842c8a41f Episode list fix rendering 2024-12-09 04:01:59 +11:00
Alex Kim
4d38573973 Fixed rubber banding issue 2024-12-09 03:38:22 +11:00
Alex Kim
785e3b6859 Stop websocket on page exit for transcoded player 2024-12-09 02:56:27 +11:00
Alex Kim
40b3304f9b Fixed socket not closing on exit 2024-12-09 02:48:36 +11:00
Fredrik Burmester
abf1b343cd Merge pull request #265 from herrrta/fix/delete-type
Fix delete by show file type
2024-12-08 16:37:21 +01:00
herrrta
e427802aae Fix delete by show file type 2024-12-08 10:34:03 -05:00
Fredrik Burmester
684e671750 fix: design issues regarding downloads 2024-12-08 16:29:17 +01:00
Fredrik Burmester
5e9b28f2eb fix: type errors and design 2024-12-08 15:59:03 +01:00
Alex Kim
1d4c56265f Made sure changes are saved when changing episode list 2024-12-09 01:54:30 +11:00
Alex Kim
1102df8384 Added fixes for opacity style 2024-12-09 01:06:32 +11:00
Fredrik Burmester
15073f47db Merge pull request #264 from Alexk2309/feature/episode-list-in-player
Feature/episode list in player
2024-12-08 14:16:58 +01:00
Alex Kim
15f32bca6c Removed useless file 2024-12-09 00:13:41 +11:00
Alex Kim
108c5f9bab Merged websocket PR 2024-12-09 00:11:19 +11:00
Fredrik Burmester
24d781050f Merge pull request #263 from fredrikburmester/fix/global-websockets-with-vlc
fix: websockets now work globally with vlc and transcoded player
2024-12-08 14:02:13 +01:00
Alex Kim
353ebf3b0c Removed opacity for unselected items 2024-12-09 00:00:03 +11:00
Fredrik Burmester
c8b16f947d fix: increase max streaming bitrate for HUGE files 2024-12-08 13:59:36 +01:00
Fredrik Burmester
bd24f59199 fix: websockets now work globally with vlc and transcoded player
does not disconnect and reconnect every time you open and close the player
2024-12-08 13:59:16 +01:00
Alex Kim
a6b49c42cf Added style changes 2024-12-08 23:50:59 +11:00
Fredrik Burmester
5afb677b3a chore 2024-12-08 11:57:47 +01:00
Alex Kim
65d3da155f Fixed style issue for devices with bottom safe area 2024-12-08 18:34:20 +11:00
Alex Kim
d616574232 Added scroll to episode when going in player mode 2024-12-08 18:25:10 +11:00
Alex Kim
b8b083abe2 Added correct starting season index 2024-12-08 18:14:41 +11:00
Alex Kim
49a1bffcf5 Added style changes for episode list 2024-12-08 18:03:06 +11:00
Alex Kim
cb6c716830 Fixed playbutton showing up on current Episode 2024-12-08 17:26:48 +11:00
Alex Kim
a725af114c Fixed playbutton showing up on current Episode 2024-12-08 17:26:17 +11:00
Alex Kim
5b290fd667 Got season dropdown to start working 2024-12-08 17:18:44 +11:00
Alex Kim
de4f60f564 WIP 2024-12-08 07:44:35 +11:00
Alex Kim
a4cd3ea600 WIP 2024-12-08 07:15:34 +11:00
Fredrik Burmester
3db12bd76a Merge pull request #261 from Alexk2309/fix/refactor-vlc-media-player
Refactored perfomance change for IOS
2024-12-07 16:53:02 +01:00
Alex Kim
26305c2983 Used debouncing for trick play and stop rendering trickplay, while not sliding 2024-12-08 02:48:23 +11:00
Alex Kim
9c02fa2e72 Refactored perfomance change for IOS 2024-12-07 23:04:21 +11:00
Fredrik Burmester
b08ec474a4 Merge pull request #259 from herrrta/fix/remux-download
Fix queue being downloaded all at once
2024-12-07 09:28:49 +01:00
Fredrik Burmester
416fb24ac0 Merge pull request #257 from Alexk2309/fix/pause-video-when-exiting-ios
Made pause video on app exit and added some performance changes for android
2024-12-07 09:28:08 +01:00
Alex Kim
0d2b15e5af Removed unneccessary print statement 2024-12-07 18:17:28 +11:00
Alex Kim
ef036cb362 Fixed for android and added some peformance changes for android 2024-12-07 18:11:41 +11:00
herrrta
006e457d23 # Fix queue being downloaded all at once 2024-12-06 19:54:43 -05:00
Alex Kim
832a717585 Improve performance of android version 2024-12-07 06:35:25 +11:00
Alex Kim
39f86a9eb1 Added android fix 2024-12-07 06:01:04 +11:00
Alex Kim
38445c6959 Fixed issue for IOS and android 2024-12-07 05:41:46 +11:00
Alex Kim
24320541c7 Made pause video on app exit on IOS 2024-12-07 04:15:16 +11:00
Fredrik Burmester
ee4e9fe347 Merge pull request #256 from Alexk2309/fix/optimize-direct-video-player-ios
Optimized direct player for IOS
2024-12-06 17:28:43 +01:00
Alex Kim
6d43b34f66 Optimized direct player for IOS 2024-12-07 03:14:25 +11:00
retardgerman
63cf7eb622 specified versions in dropdown menu 2024-12-06 14:16:53 +01:00
Fredrik Burmester
32130f1a9c Merge pull request #255 from Alexk2309/feature/prefetch-trick-play-on-video-start
Prefetch trick-play images
2024-12-06 08:39:01 +01:00
Fredrik Burmester
7f458f2f0b Merge pull request #254 from Alexk2309/hotfix/fixed-bugs-for-new-controls-ui
Bug fixes for controls
2024-12-06 08:38:48 +01:00
Alex Kim
6ec6c6daa0 Added feature to prefetch trick-play images on video start rather than downloading it while scrubbing 2024-12-06 17:41:42 +11:00
Alex Kim
02a48fd958 Refactored code, so that way the skip intro button is not using absolute positioning 2024-12-06 16:42:58 +11:00
Alex Kim
04c4dfd13a Fixed bugs for skip intro button still being able to be clicked once it is gone past the time frame 2024-12-06 15:08:33 +11:00
retardgerman
40bdb10653 add dropdown menu for App version 2024-12-05 22:59:30 +01:00
Fredrik Burmester
f16c486bfb Merge pull request #252 from Alexk2309/hotfix/control-ui-fix-skip-intro-button-overlapping-with-safe-area
Fixed skip intro button overlapping with safe area
2024-12-05 20:35:27 +01:00
Alex Kim
19fc00e314 Fixed skip intro button overlapping with safe area 2024-12-06 06:14:53 +11:00
Fredrik Burmester
c51965016c Merge pull request #251 from Alexk2309/feature/intergration-for-ios-file-provider
File System Support
2024-12-05 19:28:27 +01:00
Alex Kim
3bcf73f0dd Changed app ios settings 2024-12-06 05:11:26 +11:00
retardgerman
1ecef4be67 Update bug_report.yml 2024-12-05 18:29:59 +01:00
Fredrik Burmester
387525f9c3 Merge pull request #249 from Alexk2309/feature/subtitle-size-change
Added the ability to change subtitle size
2024-12-05 18:10:46 +01:00
Fredrik Burmester
cf182d8473 Merge branch 'master' into feature/subtitle-size-change 2024-12-05 18:10:38 +01:00
Fredrik Burmester
f0e3321a16 Merge pull request #248 from Alexk2309/feature/control-ui-change
Feature/control UI change
2024-12-05 18:09:38 +01:00
Fredrik Burmester
96c76e2b08 Merge pull request #247 from herrrta/feat/multiple-remux-downloads
Multiple Remux downloads
2024-12-05 18:09:27 +01:00
retardgerman
aaa07d93cf better bug_report.yml 2024-12-05 17:52:20 +01:00
Alex Kim
0716bba6ec Updated setting description 2024-12-06 03:47:58 +11:00
retardgerman
15476f3686 correct bug_report.yml 2024-12-05 17:44:15 +01:00
Alex Kim
97cf9185d3 Added the ability to change subtitle size 2024-12-06 03:40:48 +11:00
retardgerman
c11ad17ca5 fixed bug_report.yml 2024-12-05 17:34:20 +01:00
retardgerman
b0d563bc48 new bug_report.yml 2024-12-05 17:25:55 +01:00
retardgerman
909fc84ec0 Update bug_report.yml 2024-12-05 17:15:36 +01:00
retardgerman
0400597061 finally fixed bug_report.yml 2024-12-05 17:13:01 +01:00
retardgerman
b44a5fbbba fixed bug_report.yml 2024-12-05 17:09:13 +01:00
retardgerman
a5f6ba27b1 reworked bug submit handling 2024-12-05 17:08:32 +01:00
Alex Kim
ece1b8f2b9 Removed consoled log for changing brightness 2024-12-06 03:02:11 +11:00
Alex Kim
beb6702112 merge with master 2024-12-06 02:53:28 +11:00
Alex Kim
98c0ed4ad5 Removed pink background from slider 2024-12-06 02:50:16 +11:00
Alex Kim
b3f471bfa6 Added brightness slider 2024-12-06 01:17:24 +11:00
herrrta
1a10f0debf # Multiple Remux downloads
- Added stepper component
- Disabled more download settings based on download method
- refactored useRemuxHlsToMp4.ts to allow for multiple remux downloads
2024-12-05 01:27:55 -05:00
Fredrik Burmester
ac266c6956 Merge pull request #243 from herrrta/feat/show-overall-download-size
Show app usage
2024-12-03 16:23:24 +01:00
herrrta
b23a50914c Show app usage
- Added app usage to settings
- add more readable size formats
2024-12-03 09:56:07 -05:00
Fredrik Burmester
5c4a419d22 Merge branch 'pr/242' 2024-12-03 14:53:27 +01:00
Fredrik Burmester
3d034864f9 chore 2024-12-03 11:59:25 +01:00
Fredrik Burmester
ea183c426b fix: play new item when pressing play button 2024-12-03 11:59:22 +01:00
Fredrik Burmester
92be991cf7 fix: subtitles burn in for chromecast 2024-12-03 11:59:13 +01:00
herrrta
b73c29221a New delete options & storage visibility
- Added react-native-progress dependency
- Added bottom sheet to downloads page to handle actions for deleting items by type
- Added ability to long press to delete a single series
- Added ability to delete by season
- Refactored delete helpers in DownloadProvider.tsx
- Display storage usage inside downloads & settings page
- Fixed Delete all downloaded files from delting user data in mmkv
2024-12-02 22:37:59 -05:00
Alex Kim
880a739dd4 Reworked controls to have pause, and skip not in the slider anymore 2024-12-03 03:56:55 +11:00
Fredrik Burmester
69ffdc2ddf fix: try to fix #239 2024-12-02 12:04:24 +01:00
Fredrik Burmester
d686bd8c7b fix: tab bar icon not hiding 2024-12-02 11:44:52 +01:00
Fredrik Burmester
c8a60e735b Merge branch 'pr/238' 2024-12-02 11:35:22 +01:00
Fredrik Burmester
05f7574e60 fix: hide tab bar icon 2024-12-02 11:34:57 +01:00
Fredrik Burmester
11b880863c Revert "feat: cache item size"
This reverts commit aec172d8f5.
2024-12-02 11:34:49 +01:00
Fredrik Burmester
aec172d8f5 feat: cache item size 2024-12-02 11:13:05 +01:00
herrrta
7b52528d72 # Persist DownloadedItem size when downloading or when reading file for the first time 2024-12-01 18:07:34 -05:00
Fredrik Burmester
5fd1d9080e chore 2024-12-01 23:08:21 +01:00
Fredrik Burmester
5cc0f381fa chore 2024-12-01 22:26:40 +01:00
Fredrik Burmester
0f547deb39 Merge branch 'pr/210' 2024-12-01 21:18:20 +01:00
Fredrik Burmester
5aeb80348a chore 2024-12-01 21:16:53 +01:00
Fredrik Burmester
1dfc0ac762 Merge branch 'pr/233' 2024-12-01 21:01:59 +01:00
Fredrik Burmester
2b8aee442a Merge branch 'pr/232' 2024-12-01 21:01:54 +01:00
Fredrik Burmester
3e45adfeb5 Merge branch 'pr/231' 2024-12-01 21:01:45 +01:00
herrrta
b41363d347 # Allow option for viewing custom menu links
- Added new 'Other' setting to toggle new tab visibility
- Added new Tab to show custom links
- Added icon asset for list
2024-12-01 14:26:49 -05:00
herrrta
2d5a27c015 # Add Button to download whole series/Season
- Refactored DownloadItem.tsx to be compatible with multiple items
- Updated queueActions.enqueue signature to be compatible with array of jobs
- Added download button beside season dropdown to download entire season
- Added download button to series page to download entire series
2024-12-01 14:23:38 -05:00
herrrta
b5c6403e2d # Add download size to offline media downloads
- Added getDownloadSize helper function to display media size
 in MB or GB when appropriate
2024-12-01 14:23:29 -05:00
herrrta
7eb7d17fa9 # New downloads page for downloaded TV-Series
- Renamed downloads.tsx to index.tsx
- Added new downloads/series.tsx page
- Downloading now saves series primary image
- Downloads index page now shows series primary image with downloaded episode counter
- Updated EpisodeCard.tsx to display more information
- Moved season dropdown from SeasonPicker.tsx into its own component SeasonDropdown.tsx
- Updated navigation in DownloadItem.tsx to direct to series page when a downloaded episode is clicked
2024-12-01 14:23:12 -05:00
Fredrik Burmester
3d8875208f Merge branch 'pr/235' 2024-12-01 17:48:13 +01:00
Alex Kim
e4cfb52dab Added change to show actual device name rather than platform 2024-12-02 03:41:25 +11:00
Alex Kim
879e79cc47 Fixed some edge cases with dropdown 2024-12-02 03:21:26 +11:00
Fredrik Burmester
b9abe3e7f7 fix: stop playback on back button 2024-12-01 14:20:00 +01:00
Fredrik Burmester
383062ac0d fix: cache invalidation issue 2024-12-01 10:11:24 +01:00
Fredrik Burmester
3a507b6d1b fix: correct initialization of query client (based on docs) 2024-12-01 09:46:15 +01:00
Fredrik Burmester
500005afa8 fix: burn in subs for downloads 2024-11-30 10:18:09 +01:00
Fredrik Burmester
b638743497 fix: corrected ffmpeg command 2024-11-29 09:43:54 +01:00
Fredrik Burmester
73aae1d260 fix: don't return undefined 2024-11-29 09:43:39 +01:00
Fredrik Burmester
b84e95dc54 fix: add more remux debug logging 2024-11-29 09:05:40 +01:00
Fredrik Burmester
5292d89303 chore 2024-11-28 14:55:29 +01:00
Fredrik Burmester
acd14279f4 fix: make sure always max bitrate is selected 2024-11-28 10:21:30 +01:00
Fredrik Burmester
945d553cae fix: start at 0 for downloaded content 2024-11-28 10:15:07 +01:00
Fredrik Burmester
c33890fb38 fix: download all embeded audio and subtitle tracks, not just default 2024-11-28 10:14:57 +01:00
Fredrik Burmester
c718f53109 chore 2024-11-28 09:53:16 +01:00
Fredrik Burmester
18552bf622 Merge branch 'pr/226' 2024-11-27 12:54:24 +01:00
Fredrik Burmester
ec5c367438 Merge branch 'feature/vlc-support-android' of https://github.com/Alexk2309/streamyfin into pr/226 2024-11-27 12:25:46 +01:00
Fredrik Burmester
ba38fe6c03 fix: stop playback when exit 2024-11-27 12:25:39 +01:00
Alex Kim
a37da8f667 Added crash fix for subtitle disable 2024-11-27 22:07:48 +11:00
Fredrik Burmester
8b0b3d8abc fix: inf loop dep 2024-11-27 11:38:31 +01:00
Fredrik Burmester
d113729b6f fix: refetch on focus 2024-11-27 09:57:03 +01:00
Fredrik Burmester
e6ea5d13d4 fix: progress data not updating due to enabled being set
https://stackoverflow.com/a/72230424
2024-11-27 09:36:52 +01:00
Fredrik Burmester
c911a3c38a chore 2024-11-27 09:35:53 +01:00
Fredrik Burmester
a1a895815a fix: playback stopped dep 2024-11-27 09:35:26 +01:00
Fredrik Burmester
ea06efb82e fix: playback stopped dep 2024-11-27 09:35:17 +01:00
Fredrik Burmester
8a655c04b2 fix: key error 2024-11-27 08:59:41 +01:00
Fredrik Burmester
2db4effef5 fix: loader position 2024-11-27 08:59:31 +01:00
Fredrik Burmester
88a3bdd891 chore 2024-11-26 22:27:08 +01:00
Fredrik Burmester
6df20f516c fix: pressin to show controls 2024-11-26 22:27:05 +01:00
Fredrik Burmester
1fdf45daa7 fix: try to fix invalidate cache in prod 2024-11-26 21:54:35 +01:00
Fredrik Burmester
e8f4ee2264 chore 2024-11-26 21:24:42 +01:00
Fredrik Burmester
81d4e778e3 fix: lower opacity for video when show controls 2024-11-26 21:24:39 +01:00
Fredrik Burmester
025ce45e33 fix: routing 2024-11-25 21:59:48 +01:00
Fredrik Burmester
4f72cacbc0 fix: remove items 2024-11-25 21:59:38 +01:00
Fredrik Burmester
8c909e17bd Merge branch 'feature/vlc-support-android' of https://github.com/Alexk2309/streamyfin into pr/226 2024-11-25 17:11:13 +01:00
Fredrik Burmester
98fbf71ff8 feat: closes pr/181 2024-11-25 17:11:08 +01:00
Alex Kim
bf0c8a8007 Fixed refresh not refetching next-up and continue watching 2024-11-26 02:55:42 +11:00
Fredrik Burmester
44e5436c3b fix: video not quitting when leaving player route 2024-11-25 16:54:49 +01:00
Fredrik Burmester
d22f047f2b fix: controls safe areas 2024-11-25 15:58:08 +01:00
Fredrik Burmester
7f9dd4e14e fix: header padding 2024-11-25 08:22:04 +01:00
Alex Kim
e82890d7ff Fixed reloading issue 2024-11-25 15:53:15 +11:00
Alex Kim
0054095b20 Fixed bug with dupe subtitle names for transcoded content 2024-11-25 15:35:05 +11:00
Fredrik Burmester
d218d0b1c2 Merge branch 'feature/vlc-support-android' of https://github.com/Alexk2309/streamyfin into pr/226 2024-11-24 19:35:40 +01:00
Fredrik Burmester
93d117640a fix: change to mmkv and fix downloads with VLC 2024-11-24 19:34:49 +01:00
Alex Kim
d4009040d8 Fixed bug in code 2024-11-25 04:51:20 +11:00
Alex Kim
3d8e4a07ce Subtitle and audio now show which one is selected 2024-11-25 04:49:10 +11:00
Alex Kim
726301aca8 Merge branch 'feature/vlc-support-android' of github.com:Alexk2309/streamyfin into feature/vlc-support-android 2024-11-25 03:50:11 +11:00
Alex Kim
887ef10265 Implemented sorting subtitles in the correct order 2024-11-25 03:49:37 +11:00
Alex Kim
d47dd633c7 Updated IOS to work with new react-video transcoded player 2024-11-25 03:32:28 +11:00
Alex Kim
835484b367 Made transcoding content use react-native-video insted 2024-11-25 03:22:04 +11:00
Fredrik Burmester
335765993d fix: header issue 2024-11-24 10:51:01 +01:00
Fredrik Burmester
734772fb92 chore 2024-11-24 10:50:19 +01:00
Fredrik Burmester
56b37a1ec1 Merge branch 'pr/226' into feat/downloads-with-vlc 2024-11-24 10:38:10 +01:00
Fredrik Burmester
6a50eb9044 fix: offline playback using player component 2024-11-24 10:36:06 +01:00
Fredrik Burmester
3dee8ba2e3 chore: remove unused files 2024-11-24 09:40:26 +01:00
Fredrik Burmester
dc73677876 fix: chosenAudioTrack should not be undefined ? 2024-11-24 09:39:14 +01:00
Fredrik Burmester
0633d60186 chore 2024-11-24 00:12:14 +01:00
Fredrik Burmester
55f8af7069 wip 2024-11-23 22:42:04 +01:00
Alex Kim
02f4e4a16b Added disable option when on image based subg on transcoding 2024-11-24 01:59:22 +11:00
Alex Kim
c56b80889f Fixed up style 2024-11-24 01:15:01 +11:00
Alex Kim
ad2bfd8f28 Got preselect audio selection for IOS and android for direct play working 2024-11-24 01:02:00 +11:00
Alex Kim
0418cffba1 Added subtitle preselection for IOS 2024-11-24 00:43:07 +11:00
Alex Kim
6a29a10d82 Finished 2024-11-24 00:00:14 +11:00
Alex Kim
c5077953a8 Got preselect subtitles working for Android 2024-11-23 22:37:16 +11:00
Alex Kim
0e720aa8cf In progress of handling subtitles for transcoded streams 2024-11-23 06:17:38 +11:00
Fredrik Burmester
4699ee9c18 fix: updated device profile 2024-11-21 21:59:59 +01:00
Alex Kim
a7dd74e7ab Updated comments 2024-11-22 04:52:10 +11:00
Alex Kim
2a52499a75 Fixed HLS starting from the earliest segment for android 2024-11-22 04:51:11 +11:00
Alex Kim
a3f8087ccc Fixed HLS starting from the earliest segment 2024-11-22 01:58:59 +11:00
Fredrik Burmester
73acca6c21 fix: correct order of methods 2024-11-21 11:41:04 +01:00
Fredrik Burmester
f2367d3f68 fix: show correct list of subs 2024-11-21 10:24:26 +01:00
Fredrik Burmester
868c046cd2 fix: external subs not showing up due to isExternal not showing true when delivery url is present 2024-11-21 10:04:03 +01:00
Alex Kim
52b5b2875c WIP 2024-11-21 16:09:56 +11:00
Alex Kim
1aed133a67 Fixed not direct playing 2024-11-19 13:35:33 +11:00
Alex Kim
f127ee2976 Fixed destuctor bug 2024-11-19 02:55:01 +11:00
Alex Kim
72410d2729 Fixed next up not working 2024-11-18 20:21:34 +11:00
Fredrik Burmester
dcf59ac18e fix: ios header right padding 2024-11-18 09:24:43 +01:00
Fredrik Burmester
6b7bbf716c chore: remove unused code 2024-11-18 09:13:18 +01:00
Fredrik Burmester
6224f8b92d fix: use correct device profile for android 2024-11-18 09:09:25 +01:00
Fredrik Burmester
3843bf1fcd chore: remove unused code 2024-11-18 09:09:10 +01:00
Fredrik Burmester
5c44db183a chore 2024-11-18 08:55:34 +01:00
Fredrik Burmester
2350f4e294 fix: bitrate value not set on play start 2024-11-18 08:54:21 +01:00
Alex Kim
7ce3bc6e92 Fixed plugin not resolved issue 2024-11-18 18:07:59 +11:00
Alex Kim
21fbe1adae Try fix android issue for player seelct 2024-11-18 18:00:30 +11:00
Fredrik Burmester
cef1327fcb fix: hide/show now works + added back dropdown menu 2024-11-17 21:36:52 +01:00
Fredrik Burmester
a5677aae86 fix: move logic back into page
no need for separate ios and android components not that the player is combined
2024-11-17 21:36:40 +01:00
Fredrik Burmester
44a7ec238f fix: show distinct people 2024-11-17 21:36:13 +01:00
Alex Kim
34d7ab5f1e Fixed not starting at the correct posistion when playing video 2024-11-18 04:24:58 +11:00
Alex Kim
991f58cf73 SetSubtitle URL works now 2024-11-18 04:03:11 +11:00
Alex Kim
558480ea9d Got working subtitles/audio 2024-11-18 03:06:29 +11:00
Alex Kim
6b751cf154 Attempt to get events working 2024-11-17 20:52:07 +11:00
Alex Kim
e010c8229c Downgraded version for better compabilitiy 2024-11-17 18:35:20 +11:00
Alex Kim
128c369e55 Attempt to remove weird stretching 2024-11-17 14:18:45 +11:00
Alex Kim
0b0afb448d WIP 2024-11-17 05:48:29 +11:00
Fredrik Burmester
3d20b7956f wip 2024-11-11 09:18:49 +01:00
Fredrik Burmester
1fdf7ca42f wip 2024-11-10 23:29:21 +01:00
Fredrik Burmester
865fbdf834 wip 2024-11-10 22:36:03 +01:00
Fredrik Burmester
8ed81fbe23 wip 2024-11-10 17:03:15 +01:00
Fredrik Burmester
817e2b3d85 wip 2024-11-10 15:21:30 +01:00
Fredrik Burmester
fff880e708 fix: offline vlc playback not working 2024-11-10 11:54:22 +01:00
Fredrik Burmester
f2bcd2c675 fix: report playback stopped 2024-11-10 10:58:45 +01:00
Fredrik Burmester
00a296cee6 fix: not setting start position on video start 2024-11-10 10:55:22 +01:00
Fredrik Burmester
33b94105c2 chore 2024-11-10 10:16:31 +01:00
Fredrik Burmester
a23e370deb chore 2024-11-10 10:13:34 +01:00
Fredrik Burmester
d95833335e chore: merge things 2024-11-10 10:11:00 +01:00
Fredrik Burmester
5e91f45e3d chore 2024-11-10 09:32:48 +01:00
Fredrik Burmester
b8111babd2 Merge branch 'feat/native-tabbar' into pr/178 2024-11-10 09:31:53 +01:00
Fredrik Burmester
87092fed48 fix: working material3 theme 2024-11-10 09:21:51 +01:00
Fredrik Burmester
65a6ab9972 wip 2024-11-04 22:08:01 +00:00
Fredrik Burmester
8943708ff5 fix: update versions and use native-edge-to-edge 2024-11-04 21:35:26 +00:00
Alex Kim
c0b2579fdd Removed debug print statement 2024-11-04 00:19:58 +11:00
Alex Kim
272b8b914f Fixed incorrect time shown when downloading 2024-11-04 00:15:57 +11:00
Alex Kim
4eb7d0f151 Fixed skip intro skipping more than the video length 2024-11-03 23:18:02 +11:00
Fredrik Burmester
229670e829 fix(android): buffer state and video not loading 2024-11-01 16:54:20 +01:00
retardgerman
341a0f21d7 fix: change Icon Patch (#211) 2024-10-30 10:51:07 +01:00
Mateusz Kukieła
91b4e403e6 feat: add MacOS fullscreen support 2024-10-29 11:26:15 +01:00
Alex Kim
152d3a9c1c Fixed next up and previous episodes 2024-10-29 02:26:07 +11:00
Alex Kim
ad43ee7585 Fixed subtitle sizes 2024-10-29 00:49:22 +11:00
Alex Kim
a1fe226d22 Added hours for trickplay time 2024-10-28 23:28:59 +11:00
Alex Kim
0bc7bbed5a Fixed inproper conversion of secondsToMs 2024-10-28 21:46:46 +11:00
Alex Kim
0f178a502b Fixed intro skipper for VLC 2024-10-28 21:40:09 +11:00
Alex Kim
db20fffeb5 Fixed trick play for VLC 2024-10-28 21:12:42 +11:00
Fredrik Burmester
9ca71dc7fc Merge branch 'feature/vlc-support' of https://github.com/Alexk2309/streamyfin into pr/178 2024-10-27 15:47:07 +01:00
Fredrik Burmester
0117c87a55 fix: wrong time conversion in report playback progress 2024-10-27 15:46:44 +01:00
Fredrik Burmester
120fd4a32b wip 2024-10-27 15:21:59 +01:00
Fredrik Burmester
06e657dc4d wip 2024-10-27 14:14:27 +01:00
Fredrik Burmester
f5857e2162 fix: native pill look on android 2024-10-27 12:37:46 +01:00
Alex Kim
30280db810 WIP bug fixing 2024-10-27 22:30:30 +11:00
Fredrik Burmester
d1221dae83 chore 2024-10-27 11:24:49 +01:00
Fredrik Burmester
b99c18c5ac chore 2024-10-27 11:23:24 +01:00
Fredrik Burmester
25544eb157 chore 2024-10-27 11:22:58 +01:00
Fredrik Burmester
0f1ee174a0 chore 2024-10-27 11:21:59 +01:00
Fredrik Burmester
786f91ab4d Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-10-27 09:04:50 +01:00
Fredrik Burmester
5baa2a3697 chore: update deps 2024-10-27 09:04:37 +01:00
Alex Kim
b9375c1d7b Fixed race condition issue when playing media 2024-10-27 00:34:51 +11:00
retardgerman
d393bc0ac5 Typo fixed #206 2024-10-24 18:02:37 +02:00
Fredrik Burmester
ef9ed647c9 fix: don't include android build 2024-10-21 16:06:34 +02:00
Fredrik Burmester
68d32bd0de wip 2024-10-21 16:05:36 +02:00
Fredrik Burmester
ba76f2444d wip 2024-10-19 21:20:50 +02:00
Fredrik Burmester
d9fde3ba79 wip 2024-10-19 21:20:11 +02:00
Fredrik Burmester
f5b05bf32d wip 2024-10-19 13:20:38 +02:00
Fredrik Burmester
f71eb0be5a wip 2024-10-19 09:18:03 +02:00
Fredrik Burmester
3989d5e525 wip 2024-10-18 23:16:32 +02:00
Fredrik Burmester
4ad67f7f77 wip 2024-10-18 22:36:02 +02:00
Fredrik Burmester
39c49d4cdb wip 2024-10-18 22:27:26 +02:00
Fredrik Burmester
6e669b2aa9 fix: pause playback while seeking 2024-10-17 09:10:09 +02:00
Fredrik Burmester
ac4ce2934c fix: start position and errors 2024-10-16 18:58:45 +02:00
Fredrik Burmester
6a4fe83fbb chore 2024-10-16 08:50:22 +02:00
Fredrik Burmester
04e31e8628 fix: ignore android build of vlc player
since we don't play to have vlc for android right now
2024-10-16 08:23:45 +02:00
Fredrik Burmester
0fb2a6d32b chore: renaming refactoring 2024-10-16 08:21:57 +02:00
Fredrik Burmester
fcffee1981 feat: support start time 2024-10-16 08:21:42 +02:00
Fredrik Burmester
951a9d08ba fix: platform sepcific playing 2024-10-15 22:41:19 +02:00
Fredrik Burmester
3916c94f36 wip 2024-10-15 20:00:17 +02:00
Fredrik Burmester
c7901c759a chore 2024-10-15 13:12:25 +02:00
Fredrik Burmester
e852e40503 Merge branch 'master' into pr/178 2024-10-15 13:12:21 +02:00
Fredrik Burmester
ac9bcbcb9f fix: always use native device profile 2024-10-15 13:08:03 +02:00
Fredrik Burmester
9e5aa16a7d feat: select audio 2024-10-15 08:31:36 +02:00
Fredrik Burmester
ae963751cf wip 2024-10-15 08:01:12 +02:00
Fredrik Burmester
13d4117cc1 wip: external subs and cleanup 2024-10-15 07:32:25 +02:00
Fredrik Burmester
3807f847fd wip 2024-10-14 18:30:01 +02:00
Fredrik Burmester
67be97d857 wip: subtitles and onVideoLoad stuff 2024-10-14 11:14:34 +02:00
Alex Kim
af9f722b53 Removed duplicate Invalidate queries for next-up 2024-10-14 19:24:13 +11:00
Fredrik Burmester
092f5e73d7 Merge branch 'feature/vlc-support' of https://github.com/Alexk2309/streamyfin into pr/178 2024-10-13 20:18:38 +02:00
Alex Kim
7fe7e4e321 Fixed buffering issue 2024-10-14 05:17:52 +11:00
Fredrik Burmester
d41040e6d3 chore: keep up to date with master 2024-10-13 19:25:32 +02:00
Fredrik Burmester
a71832c6e5 feat: initial subtitle support 2024-10-13 17:59:47 +02:00
Fredrik Burmester
eefd1d9d13 fix: local playback 2024-10-13 16:07:03 +02:00
Fredrik Burmester
bbd12c540a fix: dark overlay not disapearing 2024-10-13 14:56:50 +02:00
Fredrik Burmester
0c436408e7 wip 2024-10-13 13:59:27 +02:00
Fredrik Burmester
ebc77f4ee2 Merge branch 'master' into feat/native-tabbar 2024-10-13 11:54:22 +02:00
Fredrik Burmester
d6ee1807f3 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-10-13 11:39:54 +02:00
Fredrik Burmester
0d7c3cb9da fix: again remove animations because it causes no-tap-register bug 2024-10-13 11:39:51 +02:00
Fredrik Burmester
fd252247aa fix: screen rotation issues 2024-10-13 11:39:25 +02:00
Fredrik Burmester
c12af2efe9 Merge pull request #183 from simoncaron/feat/hide-zero-hour
feat: Hide 0h on play button when media length is lower than an hour
2024-10-13 08:51:27 +02:00
Simon Caron
04b24ee86b feat: Hide 0h on play button when media length is lower than an hour 2024-10-12 14:27:51 -04:00
Alex Kim
43d64bc3d0 Solved buffering issue when paused and scrubbing 2024-10-13 01:54:41 +11:00
Fredrik Burmester
f7401bd60c fix: app crashing on video exit 2024-10-12 16:09:59 +02:00
Fredrik Burmester
6a3d0ae296 fix: play states working 2024-10-12 15:53:25 +02:00
Fredrik Burmester
5c3da8b01a fix: don't clip bottom - allow blur 2024-10-12 15:14:52 +02:00
Fredrik Burmester
46c4b3e1d8 Merge branch 'master' into feat/native-tabbar 2024-10-12 15:03:31 +02:00
Fredrik Burmester
43d251fcda chore 2024-10-12 15:02:46 +02:00
Fredrik Burmester
fed3725733 fix: use better screen dimensions 2024-10-12 15:02:23 +02:00
Fredrik Burmester
f5be204ac8 fix: cache item count 2024-10-12 15:02:10 +02:00
Fredrik Burmester
ba6322bb1f wip 2024-10-12 13:41:09 +02:00
Fredrik Burmester
bf8687a473 wip 2024-10-12 12:55:45 +02:00
Alex Kim
09c6ad47d5 Update visual playback when exiting video 2024-10-12 19:07:16 +11:00
Fredrik Burmester
091a8ff6c3 fix: show more info in debug component 2024-10-11 22:24:23 +02:00
Fredrik Burmester
cab5693ced fix: debounce updates when seeking 2024-10-11 22:20:17 +02:00
Fredrik Burmester
be867a3b10 working 2024-10-11 22:10:47 +02:00
Fredrik Burmester
093fdcda45 Merge pull request #177 from fredrikburmester/fix/video-rotation-bug
fix: video rotation bug by updating screen dimensions dynamically using an event listener
2024-10-11 19:03:35 +02:00
Fredrik Burmester
eeaa027579 fix: turn into hook 2024-10-11 19:03:15 +02:00
Fredrik Burmester
a4c20981cf Merge pull request #174 from jakequade/fix/issue-159-chromecast-button-android
fix: casting broken on 0.17.0
2024-10-11 19:01:41 +02:00
Alex Kim
57354e6b06 WIP 2024-10-12 03:00:26 +11:00
Fredrik Burmester
63965c9e64 fix: video rotation bug 2024-10-11 16:41:44 +02:00
Fredrik Burmester
c5f39f6f8a chore: no need for these props any more 2024-10-11 12:04:15 +02:00
Fredrik Burmester
eb841601f6 fix: use correct url for chromecast streaming 2024-10-11 12:04:15 +02:00
jakequade
3f5ce6dc43 use cast button rather than feather icon for casting 2024-10-11 20:34:01 +11:00
Fredrik Burmester
2ed29e5a18 wip 2024-10-10 23:11:40 +02:00
Fredrik Burmester
380172c5ac Merge branch 'master' into feat/native-tabbar 2024-10-10 23:11:35 +02:00
Fredrik Burmester
b73a33b05b chore 2024-10-10 17:27:21 +02:00
Fredrik Burmester
e3baa2f58b fix: rotation issues 2024-10-10 17:27:17 +02:00
Alex Kim
8be1e2df0c Push to remote repo 2024-10-10 23:40:01 +11:00
Fredrik Burmester
ef7fbc985f chore 2024-10-10 10:10:24 +02:00
Fredrik Burmester
381c6701f2 chore: version bump 2024-10-10 07:56:23 +02:00
Fredrik Burmester
71da79ee6a chore 2024-10-09 20:23:53 +02:00
Fredrik Burmester
5cff323871 feat: go to next episode on end 2024-10-09 20:23:50 +02:00
Fredrik Burmester
39b7c66d34 fix: don't crash app when no media source found for unmatched items 2024-10-09 20:23:40 +02:00
Fredrik Burmester
57201f8606 wip 2024-10-09 20:06:30 +02:00
Fredrik Burmester
0a098bf26e chore 2024-10-09 07:28:53 +02:00
Fredrik Burmester
f6cb90e5dc feat: add logo to login 2024-10-09 07:28:49 +02:00
Fredrik Burmester
eba9163ce8 wip 2024-10-08 22:24:52 +02:00
Fredrik Burmester
4b166cf1d8 first commit 2024-10-08 21:21:12 +02:00
Fredrik Burmester
b878e93dec Merge pull request #163 from Alexk2309/master
Removed resumable items from next up
2024-10-08 19:55:28 +02:00
Fredrik Burmester
66cd36a899 feat: native selectable text for titles 2024-10-08 19:53:48 +02:00
Fredrik Burmester
91b926e6c2 fix: larger tap area 2024-10-08 19:53:38 +02:00
Fredrik Burmester
d4cc7499c0 Merge pull request #165 from fredrikburmester/refactor/player
Refactor: Update the player logic (video, music, live-tv)
2024-10-08 18:54:43 +02:00
Fredrik Burmester
317e719460 wip 2024-10-08 18:43:25 +02:00
Alex Kim
6012f8c8d2 Removed resumable items from next up 2024-10-09 02:40:11 +11:00
Fredrik Burmester
ec0843d737 wip 2024-10-08 15:39:44 +02:00
Fredrik Burmester
a5b4f6cc78 wip 2024-10-07 10:00:16 +02:00
Fredrik Burmester
4b60de4d43 fix: padding 2024-10-07 08:18:24 +02:00
Fredrik Burmester
aa56749402 wip 2024-10-06 22:48:31 +02:00
Fredrik Burmester
d6f02bd970 wip 2024-10-06 16:33:29 +02:00
Fredrik Burmester
862e783de1 wip 2024-10-06 15:11:06 +02:00
Fredrik Burmester
0233862fc1 wip 2024-10-06 13:03:16 +02:00
Fredrik Burmester
cc242a971f Merge branch 'master' into refactor/player 2024-10-06 09:48:42 +02:00
Fredrik Burmester
4fc3044838 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-10-05 21:28:11 +02:00
Fredrik Burmester
df6cd17099 chore 2024-10-05 21:28:07 +02:00
Fredrik Burmester
5e8a0a9fa9 Merge pull request #156 from fredrikburmester/feat/live-tv
feat: live-tv support
2024-10-05 20:01:49 +02:00
Fredrik Burmester
005938a421 chore 2024-10-05 20:01:34 +02:00
Fredrik Burmester
81aafa26d4 chore: small fixes 2024-10-05 19:19:34 +02:00
Fredrik Burmester
0080874213 fix: pages 2024-10-05 19:19:28 +02:00
Fredrik Burmester
aa89c66e6e Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-10-05 13:26:46 +02:00
Fredrik Burmester
b1e2020b43 feat: new icons 2024-10-05 13:26:44 +02:00
retardgerman
5ab53738d5 fix: international App Store link 2024-10-05 13:18:49 +02:00
Fredrik Burmester
95de03f8b1 chore 2024-10-05 13:15:33 +02:00
Fredrik Burmester
48570489d5 chore 2024-10-05 12:38:02 +02:00
Fredrik Burmester
2c14a18e53 chore 2024-10-05 12:22:35 +02:00
Fredrik Burmester
200ccc6070 feat: guide grid view 2024-10-05 12:18:47 +02:00
Fredrik Burmester
1c20a3453f fix: streaming live tv now works 2024-10-05 10:24:49 +02:00
Fredrik Burmester
bf1efd7ca2 chore 2024-10-05 09:29:42 +02:00
Fredrik Burmester
387add4c83 first commit 2024-10-05 09:17:54 +02:00
Fredrik Burmester
d064622055 fix: add key 2024-10-05 08:38:24 +02:00
Fredrik Burmester
a04296f395 fix: bottom overlapp 2024-10-05 08:38:10 +02:00
Fredrik Burmester
eb11b928af fix: update device profile 2024-10-04 16:51:26 +02:00
Fredrik Burmester
4fd67091ea fix: change to ISO 639-2 Code 2024-10-04 16:48:33 +02:00
Fredrik Burmester
57c911cc94 feat: add star history 2024-10-04 16:43:58 +02:00
Fredrik Burmester
61255e6dc4 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-10-04 13:29:19 +02:00
Fredrik Burmester
d9037e72f0 chore 2024-10-04 13:29:16 +02:00
Fredrik Burmester
b25cdce702 first commit 2024-10-04 12:08:45 +02:00
Fredrik Burmester
a85d5bbc92 Update README.md 2024-10-04 08:25:09 +02:00
Fredrik Burmester
097c428d41 chore 2024-10-03 21:44:54 +02:00
Fredrik Burmester
c55d498592 Merge pull request #151 from Alexk2309/master
Added support for more libraries to use the recently added section
2024-10-03 21:39:46 +02:00
Fredrik Burmester
1ce85a9d38 Merge branch 'master' into master 2024-10-03 21:39:27 +02:00
Fredrik Burmester
027c69bb7e fix: refactor 2024-10-03 21:37:20 +02:00
Fredrik Burmester
7a20b6db7d Merge pull request #142 from Simon-Eklundh/master
set downloads button to green if downloads exist
2024-10-03 20:04:23 +02:00
Fredrik Burmester
0e8f6dc0cc fix: use hook and purple color 2024-10-03 20:02:14 +02:00
Fredrik Burmester
bc3bdbf4c5 Merge branch 'master' into pr/142 2024-10-03 19:53:09 +02:00
Fredrik Burmester
2bea483f08 Merge pull request #140 from fredrikburmester/feat/background-downloads
[Feature] Background downloads
2024-10-03 19:27:53 +02:00
Fredrik Burmester
559d8474bc wip 2024-10-03 18:26:59 +02:00
Fredrik Burmester
83548da2c5 wip 2024-10-03 07:46:09 +02:00
Fredrik Burmester
b21a1cd18e wip 2024-10-03 07:37:37 +02:00
Fredrik Burmester
60981504fc wip 2024-10-02 22:07:13 +02:00
Alex Kim
f4bf0b2773 Fixed bug with useMemo changed size 2024-10-03 05:54:37 +10:00
Alex Kim
8138b37e7a Added support for more libaries to use the recently added section 2024-10-03 05:35:07 +10:00
Fredrik Burmester
1df7d8e8fe wip 2024-10-01 22:59:33 +02:00
Fredrik Burmester
0acc1f03f0 wip 2024-10-01 17:42:09 +02:00
Fredrik Burmester
dd1f02a13b fix: manual download button 2024-10-01 14:16:02 +02:00
Fredrik Burmester
c5c5252b89 fix: music screen fixes #149 2024-10-01 10:12:51 +02:00
Fredrik Burmester
679d6078e2 fix: remove failed process 2024-09-30 22:49:47 +02:00
Fredrik Burmester
329a75a047 fix: download path on android 2024-09-30 22:11:53 +02:00
Fredrik Burmester
5f91712126 fix: keep track of local download progress 2024-09-30 20:48:39 +02:00
Fredrik Burmester
7ce2c90376 wip 2024-09-30 16:34:54 +02:00
Fredrik Burmester
0263ad6109 fix: bottom padding 2024-09-29 23:27:16 +02:00
Fredrik Burmester
05b7872022 fix: use sheet instead of context 2024-09-29 23:25:10 +02:00
Fredrik Burmester
9458d113de wip 2024-09-29 22:56:22 +02:00
Fredrik Burmester
12cb6d4963 wip 2024-09-29 19:43:17 +02:00
Fredrik Burmester
1c2578477a fix: app not reporting playback started on first start 2024-09-29 14:48:51 +02:00
Fredrik Burmester
c7e10a13b5 fix: download progress not showing 2024-09-29 13:28:36 +02:00
Fredrik Burmester
31dbd84bec fix: add back speed to normal downloads 2024-09-29 12:03:37 +02:00
Fredrik Burmester
b6c6bac06a fix: queue now work for normal downloads 2024-09-29 11:59:37 +02:00
Fredrik Burmester
b0f7cfd013 wip 2024-09-29 11:02:13 +02:00
Fredrik Burmester
456048a92c wip 2024-09-29 08:05:22 +02:00
Fredrik Burmester
ff88c45d43 wip 2024-09-28 20:24:39 +02:00
Fredrik Burmester
ddcb410df6 wip 2024-09-28 19:57:56 +02:00
Fredrik Burmester
b3a938b53a wip 2024-09-28 18:32:08 +02:00
Fredrik Burmester
73c43d31ee wip 2024-09-28 16:13:01 +02:00
Fredrik Burmester
41a23d3437 wip 2024-09-28 15:45:00 +02:00
Simon Eklundh
92ebb29808 Merge branch 'master' into master 2024-09-28 10:52:03 +02:00
Simon Eklundh
f46cb97e7f working prototype 2024-09-28 10:42:02 +02:00
Fredrik Burmester
79020c357f wip 2024-09-27 18:44:41 +02:00
Fredrik Burmester
a386c3a47c wip 2024-09-27 18:18:10 +02:00
Fredrik Burmester
a46737442d wip 2024-09-27 17:35:51 +02:00
Fredrik Burmester
41d209f3b7 wip 2024-09-27 15:56:42 +02:00
Fredrik Burmester
d7eb25edf4 Update README.md 2024-09-26 13:34:19 +02:00
Fredrik Burmester
d672882c4b Update README.md 2024-09-26 13:34:03 +02:00
Fredrik Burmester
09dafea4ad chore 2024-09-25 08:28:36 +02:00
Fredrik Burmester
8936a559de feat: fade in player 2024-09-25 08:28:26 +02:00
Fredrik Burmester
7c10c467f3 chore 2024-09-25 07:42:41 +02:00
Fredrik Burmester
eff12b7350 fix: pip 2024-09-25 07:42:36 +02:00
Fredrik Burmester
94b6de6066 fix: open new full screen player 2024-09-25 07:42:32 +02:00
Fredrik Burmester
e82b154032 feat: skip credits 2024-09-24 20:05:04 +02:00
Fredrik Burmester
dd2a869929 chore: refactor 2024-09-24 20:04:58 +02:00
Fredrik Burmester
cd6158e141 chore 2024-09-24 20:04:50 +02:00
Fredrik Burmester
7fd232614b fix: refactor 2024-09-24 18:16:54 +02:00
Fredrik Burmester
af0a5f54d8 fix: design 2024-09-24 18:16:45 +02:00
Fredrik Burmester
3151812325 chore 2024-09-24 18:16:38 +02:00
Fredrik Burmester
9aa0dc0a3d wip: full screen player 2024-09-24 18:16:26 +02:00
Fredrik Burmester
9fcff04c0d chore: hide button 2024-09-24 18:16:00 +02:00
Fredrik Burmester
d1b6a265a1 fix: #135 2024-09-24 18:15:52 +02:00
Fredrik Burmester
ff1decfe2c feat: select skip/rewind time + refactor video player 2024-09-22 23:05:13 +02:00
Fredrik Burmester
a023c91877 feat: add genres to movies and episodes 2024-09-20 07:13:47 +02:00
Fredrik Burmester
27acd5287f fix: smaller card 2024-09-19 22:21:01 +02:00
Fredrik Burmester
ae73dab46d fix: design improvements 2024-09-19 22:17:40 +02:00
Fredrik Burmester
11f53630b5 feat: more from this actor 2024-09-19 22:17:29 +02:00
Fredrik Burmester
b7465a94e9 chore 2024-09-19 21:23:38 +02:00
Fredrik Burmester
cc97acbd4f feat: title selectable 2024-09-19 21:23:32 +02:00
Fredrik Burmester
abf7ec7d69 feat: include size 2024-09-19 21:23:22 +02:00
Fredrik Burmester
4dc9a6a0aa fix: initial load should be true 2024-09-19 21:23:04 +02:00
Fredrik Burmester
dd05ae89c3 fix: small changes 2024-09-19 16:51:13 +02:00
Fredrik Burmester
8a60adc6b2 fix: header size 2024-09-19 15:52:37 +02:00
Fredrik Burmester
51376cc8c1 fix: remove auto-hide controls for now 2024-09-19 13:00:20 +02:00
Fredrik Burmester
4eab1ebff6 chore 2024-09-18 08:42:26 +02:00
Fredrik Burmester
76388a408c fix: throttle 2024-09-18 08:42:23 +02:00
Fredrik Burmester
d3560c287c fix: more languages 2024-09-18 08:42:18 +02:00
Fredrik Burmester
da78ce898c fix: design 2024-09-18 08:42:09 +02:00
Fredrik Burmester
8a999a56a1 chore: component 2024-09-18 08:41:58 +02:00
Fredrik Burmester
669f8d7d1a fix: design 2024-09-18 08:41:48 +02:00
Fredrik Burmester
83bb5db335 fix: routing 2024-09-18 08:41:44 +02:00
Fredrik Burmester
7a5427099c fix: smaller titles 2024-09-18 08:41:28 +02:00
Fredrik Burmester
ee9b3de7d4 feat: set the server name as title 2024-09-17 19:34:32 +02:00
Fredrik Burmester
bcc28d7513 fix: accidental press when scrolling through the carousel 2024-09-17 19:34:18 +02:00
Fredrik Burmester
d093c028d2 chore 2024-09-17 19:33:46 +02:00
Fredrik Burmester
3032813234 Merge branch 'pr/129' 2024-09-17 18:55:06 +02:00
Fredrik Burmester
4a7d8721b3 chore 2024-09-17 18:50:26 +02:00
Fredrik Burmester
f45139ff90 Merge branch 'master' into feat/new-player-design 2024-09-17 18:49:44 +02:00
Fredrik Burmester
65579c88e5 wip 2024-09-17 18:49:11 +02:00
Fredrik Burmester
d716e42c20 wip 2024-09-17 08:53:10 +02:00
Fredrik Burmester
ffe1003710 wip 2024-09-17 08:31:47 +02:00
Fredrik Burmester
5c008f64b5 chore 2024-09-17 07:48:33 +02:00
Fredrik Burmester
721cd093f4 wip: orientation setting 2024-09-16 21:43:50 +02:00
Fredrik Burmester
3577aae7cc fix: improved server check
Included timeout, check url even if http is included, doc strings
2024-09-16 21:15:57 +02:00
Fredrik Burmester
402bdec5ab wip 2024-09-16 18:18:08 +02:00
simon
5e141f27c4 /System/Info/Public instead of /web 2024-09-16 16:37:45 +02:00
Fredrik Burmester
595120229f feat: see forward cached video length 2024-09-15 21:44:40 +02:00
Fredrik Burmester
09363bffdc wip 2024-09-15 21:36:59 +02:00
Fredrik Burmester
c3237571a8 fix: don't show fullscreen player on return from pip 2024-09-15 19:16:40 +02:00
simon
b67a4f1843 fixed some inconsistencies 2024-09-15 18:48:16 +02:00
simon
19a53da8a7 remove need for http/https 2024-09-15 18:45:26 +02:00
Fredrik Burmester
e3c4a291f0 wip 2024-09-15 18:39:20 +02:00
simon
25656cb7f1 remove need for http/https 2024-09-15 18:13:38 +02:00
simon
7f9c893560 remove need for http/https 2024-09-15 18:10:22 +02:00
Fredrik Burmester
ce2e5e0fb8 wip 2024-09-15 17:07:30 +02:00
Fredrik Burmester
c7703df3ce wip 2024-09-15 16:39:26 +02:00
simon
39880a6214 possible a good fix 2024-09-15 12:49:47 +02:00
Fredrik Burmester
b7629f6f2b wip: only full screen view, bar removed 2024-09-15 09:28:43 +02:00
Fredrik Burmester
409e2de6c8 wip: working small + full with transition 2024-09-15 08:56:54 +02:00
Fredrik Burmester
7cb67d73ec Merge branch 'master' into feat/new-player-design 2024-09-14 07:28:23 +03:00
Fredrik Burmester
1fe1438ecf chore: deps 2024-09-14 07:28:05 +03:00
Fredrik Burmester
611f5ae37b Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-09-14 07:22:34 +03:00
retardgerman
d2701254b3 Update bug_report.md 2024-09-13 14:08:15 +02:00
retardgerman
994dd44fc5 Update feature_request.md 2024-09-13 14:07:25 +02:00
Fredrik Burmester
f7e04dfa2d wip 2024-09-13 13:19:29 +03:00
Fredrik Burmester
cd126bb1c7 wip 2024-09-13 13:09:27 +03:00
Fredrik Burmester
ddbfb91260 wip 2024-09-13 13:07:55 +03:00
Fredrik Burmester
caac40c4b1 wip 2024-09-13 13:07:47 +03:00
Fredrik Burmester
2632feb3e8 wip 2024-09-13 12:24:25 +03:00
Fredrik Burmester
778447c1fd wip 2024-09-13 12:23:59 +03:00
Fredrik Burmester
5a1f555703 chore 2024-09-13 12:23:45 +03:00
Fredrik Burmester
2ed18d6588 feat: toast when starting download 2024-09-10 13:31:55 +03:00
Fredrik Burmester
c494b8e9f9 fix: add a test toast button 2024-09-10 13:31:36 +03:00
Fredrik Burmester
354fdd6791 fix: design 2024-09-10 13:31:20 +03:00
Fredrik Burmester
f48e0348ad fix: better play button color 2024-09-10 13:14:29 +03:00
Fredrik Burmester
23eaddf87c feat: add sonner/toast 2024-09-10 13:14:21 +03:00
Fredrik Burmester
a90dfb2805 fix: text 2024-09-10 10:27:15 +03:00
Fredrik Burmester
78d168050a chore 2024-09-10 10:26:16 +03:00
Fredrik Burmester
b92d55b9a0 chore 2024-09-10 10:25:52 +03:00
Fredrik Burmester
907f6193b5 fix: android header bugs 2024-09-10 10:25:48 +03:00
Fredrik Burmester
6f34f2e6a6 feat: allow authorizing quick connect closes #113 2024-09-09 09:04:35 +03:00
Fredrik Burmester
c25b26653e chore 2024-09-08 09:02:10 +03:00
Fredrik Burmester
5d3a1d9058 Merge pull request #122 from Simon-Eklundh/master
changes suggested for tvshows
2024-09-07 18:20:02 +03:00
Fredrik Burmester
dbaba93fbf fix: add limit
don't know if nessesary since there can only be 1 next up...?
2024-09-07 18:16:59 +03:00
Fredrik Burmester
4a1ea7ea70 fix: add api type and better undefined handling 2024-09-07 18:15:12 +03:00
Simon Eklundh
c33890a0fe Merge branch 'fredrikburmester:master' into master 2024-09-07 10:56:43 +02:00
simon
35a470c4ae possible suggested episodes bandaid 2024-09-07 10:56:05 +02:00
retardgerman
a69be4eab9 Changed typo 2024-09-06 18:04:22 +02:00
Fredrik Burmester
fced376a68 chore 2024-09-05 18:30:22 +03:00
Fredrik Burmester
848a5aac1a fix: correct text 2024-09-05 18:30:13 +03:00
Fredrik Burmester
5608646c8b chore: deps version updates 2024-09-05 09:34:26 +03:00
Fredrik Burmester
cdc3be41c1 fix: preserve sort order per library/collection
fixes #84
2024-09-04 22:49:43 +03:00
Fredrik Burmester
3f4826c4ce chore 2024-09-04 22:48:15 +03:00
Fredrik Burmester
e173d51dbb chore 2024-09-04 09:47:13 +03:00
Fredrik Burmester
b4fdbcf63d Merge branch 'wip/general-posters' 2024-09-04 09:47:04 +03:00
Fredrik Burmester
f33c4ca690 feat: no connection info 2024-09-03 18:54:17 +03:00
Fredrik Burmester
1318eafa43 chore 2024-09-03 18:53:56 +03:00
Fredrik Burmester
d222c54bae chore 2024-09-03 18:53:51 +03:00
Fredrik Burmester
f24b5612b2 chore 2024-09-03 18:53:48 +03:00
Fredrik Burmester
6713098dc7 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-09-03 17:50:12 +03:00
Fredrik Burmester
0357554f6a chore: imports 2024-09-03 17:50:07 +03:00
retardgerman
2fc9229db0 Update Screenshots 2024-09-03 14:25:23 +02:00
retardgerman
4781df0ba3 upload android screenshot 2024-09-03 14:23:41 +02:00
retardgerman
db94cfaa79 Delete assets/images/screenshots/S24-Streamyfin.png 2024-09-03 14:23:03 +02:00
retardgerman
7d5397b545 add android screenshot 2024-09-03 14:14:45 +02:00
retardgerman
fac50ed569 add new screenshots 2024-09-03 13:59:34 +02:00
Fredrik Burmester
4994df390c Merge pull request #120 from Gauvino/readme-typo
Fix typo and change sentences on README
2024-09-03 08:34:35 +02:00
Fredrik Burmester
67214a81c4 fix: can not play offline content 2024-09-03 08:55:03 +03:00
Fredrik Burmester
2509a8d6e2 feat: default sub/audio setting 2024-09-03 08:54:48 +03:00
Fredrik Burmester
d4252682be wip: use general poster component 2024-09-03 08:54:05 +03:00
Uruk
c31eb498ea fix: change typo and change sentences 2024-09-02 23:06:11 +02:00
Fredrik Burmester
7b9bad630f Merge branch 'master' into wip/general-posters 2024-09-01 20:11:48 +02:00
Fredrik Burmester
10e0a45cd4 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-09-01 17:37:33 +02:00
Fredrik Burmester
fb0b9c83ae fix: meta data (including image) when casting 2024-09-01 17:36:27 +02:00
Fredrik Burmester
58b72b8b75 fix: open expanded controls in header if casting 2024-09-01 17:36:15 +02:00
Fredrik Burmester
b771c90dfc Merge branch 'master' of https://github.com/jakequade/streamyfin into pr/106 2024-09-01 17:13:33 +02:00
Fredrik Burmester
7fa729f89f Merge branch 'master' into pr/106 2024-09-01 17:11:52 +02:00
Fredrik Burmester
682ab4dd31 Merge pull request #114 from lostb1t/feature/collectiondefault
feat: Add Default option and use collection sorting as default
2024-09-01 17:10:48 +02:00
Fredrik Burmester
3d73f604ac wip 2024-09-01 17:10:33 +02:00
jakequade
318940f7c4 remove additional play call 2024-09-01 18:21:40 +10:00
jakequade
2ee6573a90 iOS support 2024-09-01 16:26:53 +10:00
jakequade
3bd1177c45 chromecast controls 2024-09-01 16:26:51 +10:00
jakequade
080de162ec extended cast controls on android 2024-09-01 16:26:27 +10:00
Fredrik Burmester
cca28d7e21 fix: change to enums and only store key in filter state 2024-08-30 12:55:28 +02:00
Fredrik Burmester
e29b3787b9 chore 2024-08-30 12:54:53 +02:00
Fredrik Burmester
ef8bb3e717 chore 2024-08-30 12:54:38 +02:00
Fredrik Burmester
61cb205f93 fix: refactor to use enums 2024-08-30 12:54:31 +02:00
Fredrik Burmester
ffea51ccb0 chore: version 2024-08-30 10:07:39 +02:00
Fredrik Burmester
0a53cf6b17 fix: animated progress 2024-08-30 10:07:35 +02:00
sarendsen
32ac4ec62f fix: use PremiereDate as default if missing from collection 2024-08-30 10:04:02 +02:00
sarendsen
30678813b4 feat: Add Default option and use collection sorting as default 2024-08-30 09:58:50 +02:00
Fredrik Burmester
68cfe99421 fix: #95 2024-08-30 00:28:07 +02:00
Fredrik Burmester
55b1c3ae45 Reapply "fix: #104 #103 #102"
This reverts commit 6c1db4bbb9.

fix #104 fix #102 fix #103
2024-08-30 00:14:33 +02:00
Fredrik Burmester
6c1db4bbb9 Revert "fix: #104 #103 #102"
This reverts commit bbaab1994a.
2024-08-30 00:13:45 +02:00
Fredrik Burmester
bbaab1994a fix: #104 #103 #102 2024-08-30 00:13:15 +02:00
Fredrik Burmester
8c0e7f7db8 fix: item page for item not associated with movie/tv-show not loading 2024-08-29 23:03:51 +02:00
Fredrik Burmester
8b3b492f5e fix: small design fixes 2024-08-29 13:10:54 +02:00
Fredrik Burmester
78189c8246 fix: download url not correct for direct streams 2024-08-29 12:58:51 +02:00
Fredrik Burmester
dc02db6463 chore: version 2024-08-29 09:11:50 +02:00
Fredrik Burmester
c168d79377 Merge branch 'fix/landscape-design' 2024-08-29 08:45:20 +02:00
Fredrik Burmester
f756a663fe Merge branch 'fix/music-pages' 2024-08-29 08:45:07 +02:00
Fredrik Burmester
2baf57156e fix: landscape design 2024-08-29 08:44:58 +02:00
Fredrik Burmester
a97610a51d fix: audio poster + links 2024-08-29 08:44:47 +02:00
Fredrik Burmester
79b87b3d72 fix: song pages 2024-08-29 08:42:16 +02:00
Fredrik Burmester
d52f025873 fix: landscape design 2024-08-29 08:40:55 +02:00
Fredrik Burmester
30348dc28f chore: version 2024-08-28 22:11:54 +02:00
Fredrik Burmester
faf39a6de2 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-08-28 22:00:52 +02:00
Fredrik Burmester
4641ff726c wip: design refactor 2024-08-28 22:00:50 +02:00
Fredrik Burmester
8eac2f39a8 fix: move button 2024-08-28 09:33:48 +02:00
Fredrik Burmester
309345c834 chore 2024-08-27 22:32:15 +02:00
Fredrik Burmester
0d07f7216c fix: design 2024-08-27 22:32:09 +02:00
Fredrik Burmester
b550d6302f fix: #112 2024-08-27 12:06:26 +02:00
Fredrik Burmester
55ba3daf86 chore 2024-08-27 12:05:31 +02:00
Fredrik Burmester
e0afb68f0c chore 2024-08-27 12:05:25 +02:00
Fredrik Burmester
91ed109a04 feat: select media source 2024-08-27 08:26:27 +02:00
Fredrik Burmester
2565bf7353 chore: remove console.log 2024-08-26 22:17:17 +02:00
Fredrik Burmester
bbc6f63089 chore 2024-08-26 22:12:54 +02:00
Fredrik Burmester
e6d4414fd6 feat: quick connect
fixes #16
2024-08-26 21:52:23 +02:00
Fredrik Burmester
3047367ba6 fix: better posters and item screen 2024-08-26 19:47:02 +02:00
Fredrik Burmester
07c5c21599 fix: click on season number should take you to season
fixes #110
2024-08-26 08:49:42 +02:00
Fredrik Burmester
aa60e320c5 feat: split episodes and movies 2024-08-26 08:49:12 +02:00
Fredrik Burmester
c12b58e5cb fix: posters 2024-08-26 08:30:12 +02:00
Fredrik Burmester
d962507749 fix: playback pause/play issues 2024-08-26 08:30:08 +02:00
Fredrik Burmester
a351c8d220 fix: trying to fix playback issues 2024-08-26 00:02:45 +02:00
Fredrik Burmester
969e68901a fix: play flickering bug 2024-08-25 22:21:42 +02:00
Fredrik Burmester
c0f4587501 fix: select season 1 2024-08-25 21:12:20 +02:00
Fredrik Burmester
e8944528e4 fix: failover for posters 2024-08-25 20:56:52 +02:00
Fredrik Burmester
9b2185d29e fix: posters 2024-08-25 20:47:57 +02:00
Fredrik Burmester
67af14dced chore 2024-08-25 20:47:54 +02:00
Fredrik Burmester
7324fe826e fix: button not pressable 2024-08-25 18:28:48 +02:00
Fredrik Burmester
75f3f483eb chore 2024-08-25 17:58:57 +02:00
Fredrik Burmester
57cac96df5 chore 2024-08-25 17:58:53 +02:00
Fredrik Burmester
7792b8a675 feat: auto login through url 2024-08-25 17:54:42 +02:00
Fredrik Burmester
55df3991f5 chore 2024-08-25 16:59:58 +02:00
Fredrik Burmester
26057ed196 feat: more commands 2024-08-25 16:59:53 +02:00
Fredrik Burmester
30658ff067 feat: show playback % in play button 2024-08-25 16:59:33 +02:00
Fredrik Burmester
8d327e8835 fix: design and posters 2024-08-25 16:59:24 +02:00
Fredrik Burmester
b1726962c1 fix: prepare dynamic render method for use of server config 2024-08-25 14:25:26 +02:00
Fredrik Burmester
b22ffee707 Merge branch 'master' into pr/106 2024-08-25 12:13:35 +02:00
Fredrik Burmester
25e6f655f3 fix: remove wrong text for now 2024-08-25 12:13:00 +02:00
Fredrik Burmester
275923dbdd feat: add stats to rows and covers 2024-08-25 12:12:52 +02:00
Fredrik Burmester
36f1ea384d fix: return to fullscreen after pip 2024-08-25 11:48:32 +02:00
Fredrik Burmester
c100c2e0c4 fix: try to fix auto fullscreen 2024-08-25 11:48:23 +02:00
Fredrik Burmester
f9a5841f88 fix: no encryption 2024-08-25 11:48:00 +02:00
Fredrik Burmester
42f4631143 fix: don't show text unknown 2024-08-25 11:17:05 +02:00
Fredrik Burmester
638e8851c1 fix: add metadata 2024-08-25 11:15:50 +02:00
Fredrik Burmester
5c95730715 feat: prepare for force resolution 2024-08-25 11:14:43 +02:00
Fredrik Burmester
ec5aab99b8 feat: library list design 2024-08-25 10:34:54 +02:00
jakequade
688c343a35 iOS support 2024-08-25 00:08:13 +10:00
Fredrik Burmester
70d0ec4780 fix: load correct content type in collections + load content earlier 2024-08-24 13:14:51 +02:00
jakequade
fb6e3dc690 chromecast controls 2024-08-24 15:14:14 +10:00
jakequade
e9783d293d extended cast controls on android 2024-08-24 14:37:49 +10:00
Fredrik Burmester
a89d9c1f67 fix: correct page title 2024-08-23 17:00:38 +02:00
Fredrik Burmester
725ba1ccaf feat: major refactor of navigation 2024-08-23 16:41:59 +02:00
Fredrik Burmester
d330dd8db4 fix: update state on file remove 2024-08-23 08:52:16 +02:00
Fredrik Burmester
20739e6e2c feat: actor page 2024-08-23 07:51:36 +02:00
Fredrik Burmester
ec50a90a32 chore 2024-08-23 07:16:57 +02:00
Fredrik Burmester
6f6b46c14a chore 2024-08-22 16:47:58 +02:00
Fredrik Burmester
7fcdfe9452 feat: search for collections 2024-08-22 16:47:55 +02:00
Fredrik Burmester
f9af493dc8 feat: use Flashlist for smooth scrolling 2024-08-22 16:47:48 +02:00
Fredrik Burmester
e8dc9e759a fix: bug since media playback refactor 2024-08-22 16:46:53 +02:00
Fredrik Burmester
06877f4339 fix: #97 2024-08-22 14:28:39 +02:00
Fredrik Burmester
c496b1036b Merge branch 'hotfix/limit-next-up' 2024-08-22 13:28:13 +02:00
Fredrik Burmester
4cca6f0e8c fix: limit image sizes 2024-08-22 13:27:22 +02:00
Fredrik Burmester
7bf5fb9a01 chore 2024-08-22 13:12:10 +02:00
Fredrik Burmester
bbf926e752 fix: limit next up items 2024-08-22 13:11:59 +02:00
Fredrik Burmester
9b2a0487d2 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-08-22 11:28:09 +02:00
Fredrik Burmester
a73488614c fix: limit to 20 items for next up on home screen 2024-08-22 11:28:03 +02:00
Fredrik Burmester
03fdf31b4b Update README.md 2024-08-22 09:21:58 +02:00
Fredrik Burmester
2e563bcbe0 chore 2024-08-21 23:21:59 +02:00
Fredrik Burmester
b1062628d9 fix: can't play local media bug
appeared after playback rewrite
2024-08-21 23:21:53 +02:00
Fredrik Burmester
e216c8392f fix: jellysearch + chromecast 2024-08-21 23:20:40 +02:00
Fredrik Burmester
1a47ade4dc chore: version 2024-08-21 21:05:53 +02:00
Fredrik Burmester
a54da1c3dc fix: open google search for actor if Marlin turned off 2024-08-21 20:37:50 +02:00
Fredrik Burmester
874364fcde feat: new list view for seasons with persistent season state 2024-08-21 20:34:52 +02:00
Fredrik Burmester
9059f33538 fix: smaller height for dropdown menus 2024-08-21 20:34:24 +02:00
Fredrik Burmester
27785e7d18 feat: new download quality options 2024-08-21 20:34:07 +02:00
Fredrik Burmester
9a621cab4e fix: add option for height 2024-08-21 20:21:21 +02:00
Fredrik Burmester
a37ac74e9f chore 2024-08-21 20:21:06 +02:00
Fredrik Burmester
4d4bb0f6a4 chore 2024-08-21 20:21:00 +02:00
Fredrik Burmester
6bd60b2ec6 fix: #78 2024-08-21 20:20:52 +02:00
Fredrik Burmester
696a2a4780 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-08-21 18:43:56 +02:00
Fredrik Burmester
13ac9b0443 chore 2024-08-21 18:43:54 +02:00
Fredrik Burmester
83407674be fix: slow down 2024-08-21 18:43:38 +02:00
Fredrik Burmester
53bb1751c2 fix: websocket should not try to connect without token 2024-08-21 18:43:27 +02:00
Fredrik Burmester
fa31ff8f2b Merge pull request #92 from lostb1t/master
Add DateCreated sort option an disable bit rate sort
2024-08-21 18:35:31 +02:00
lostb1t
f863c95f70 Disable bitrate filter 2024-08-21 18:00:06 +02:00
lostb1t
f227565dbf Use DateCreate for sorting last added 2024-08-21 17:58:42 +02:00
Fredrik Burmester
7abc629a10 fix: #87 2024-08-21 08:01:46 +02:00
Fredrik Burmester
70a250df5b fix: #86 and add docsting 2024-08-21 07:48:45 +02:00
Fredrik Burmester
dbdf56b990 Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-08-21 07:33:33 +02:00
Fredrik Burmester
8b2204896a chore 2024-08-21 07:33:22 +02:00
Fredrik Burmester
89729a95cd Merge pull request #85 from yihaolee85/master
Update [collectionId].tsx to fix sort order
2024-08-21 07:31:38 +02:00
yihaolee85
57dd3b8446 Update [collectionId].tsx to fix sort order
Update [collectionId].tsx to fix sort order
2024-08-21 10:02:15 +08:00
Fredrik Burmester
8d2a0378ca fix: correct sf collection use 2024-08-20 22:39:36 +02:00
Fredrik Burmester
cbe01a0012 feat: use external player (vlc) 2024-08-20 21:51:16 +02:00
Fredrik Burmester
eed4df6a8a fix: make posters a bit smaller 2024-08-20 19:18:51 +02:00
retardgerman
5e081751a4 updated discord invite link 2024-08-20 11:47:40 +02:00
Fredrik Burmester
09f953ebba fix: ws now works as expexted 2024-08-20 08:42:52 +02:00
Fredrik Burmester
4873aaf3df chore 2024-08-20 08:26:02 +02:00
Fredrik Burmester
9bbab4f46f chore 2024-08-20 08:25:05 +02:00
Fredrik Burmester
469e8b3f01 refactor: playing state 2024-08-20 08:24:05 +02:00
Fredrik Burmester
1c31458dd4 chore 2024-08-19 22:52:30 +02:00
Fredrik Burmester
4c097c557f fix: #79 2024-08-19 22:52:27 +02:00
Fredrik Burmester
c23ca905c8 fix: bug after refactor 2024-08-19 22:26:33 +02:00
Fredrik Burmester
ed3170af76 fix: invalid query 2024-08-19 22:05:23 +02:00
Fredrik Burmester
e22dd759c7 fix: #71 2024-08-19 22:05:17 +02:00
Fredrik Burmester
aa44caa161 chore 2024-08-19 21:55:40 +02:00
Fredrik Burmester
27260faea8 fix: #75 2024-08-19 21:55:32 +02:00
Fredrik Burmester
ec7e5f869d feat: first implementation of ws 2024-08-19 21:45:15 +02:00
Fredrik Burmester
8e1a07e819 chore 2024-08-19 21:44:00 +02:00
Fredrik Burmester
250c1968f3 chore 2024-08-19 21:43:56 +02:00
Fredrik Burmester
caeedfbc52 fix: separate item and show bar state 2024-08-19 21:43:48 +02:00
Fredrik Burmester
66ce6b2cfa fix: refactor to work like other inf scrolling pages 2024-08-19 21:43:14 +02:00
Fredrik Burmester
388480adef feat: use Marlin 2024-08-19 21:42:45 +02:00
Fredrik Burmester
e911f99b26 chore: rename to libraries 2024-08-19 21:42:16 +02:00
retardgerman
73ff0aa66a Moved reference to APKs to the download section 2024-08-19 17:10:10 +02:00
retardgerman
29ae6747c4 Merge pull request #68 from vicegold/master
fix marketplace logo alignment
2024-08-19 17:05:12 +02:00
Laurids Düllmann
44444e3b37 fix marketplace logo alignment 2024-08-19 17:02:27 +02:00
retardgerman
0e3f289d43 alternative apple App Store badge 2024-08-19 16:48:11 +02:00
retardgerman
a66648c67c Merge pull request #65 from FintasticMan/fix-season-numbers 2024-08-18 19:43:49 +02:00
retardgerman
6dc9538483 Merge pull request #62 from lostb1t/master
Update README.md with collection info
2024-08-18 18:04:54 +02:00
Fredrik Burmester
752cb1cdc6 wip 2024-08-18 17:10:31 +02:00
FintasticMan
cb7c018cf4 fix: season number for named seasons 2024-08-18 16:15:19 +02:00
lostb1t
a01217b8ac Update README.md with collection info 2024-08-18 15:40:40 +02:00
Fredrik Burmester
21c1221138 chore 2024-08-18 13:35:23 +02:00
Fredrik Burmester
6a8a155547 chore 2024-08-18 10:52:51 +02:00
Fredrik Burmester
dbb7c6c9a5 fix: use scrollview for better design 2024-08-18 10:52:48 +02:00
Fredrik Burmester
30280e8b3a fix: icon 2024-08-18 10:52:34 +02:00
Fredrik Burmester
5281cba284 feat: inf horizontal scroll for media lists 2024-08-18 10:52:20 +02:00
Fredrik Burmester
da666d3991 fix: tight navbar on android 2024-08-18 10:51:44 +02:00
Fredrik Burmester
817a758b8a fix: double routing bug 2024-08-18 10:51:35 +02:00
Fredrik Burmester
f04a29b757 fix: loading test rollback 2024-08-18 07:45:01 +02:00
Fredrik Burmester
550fc39faa fix: loading indicator style 2024-08-18 07:44:28 +02:00
Fredrik Burmester
d56bb79ac2 feat: filters with bottom sheet 2024-08-17 23:34:51 +02:00
Fredrik Burmester
30781a6dfe wip: refactor 2024-08-17 19:11:42 +02:00
Fredrik Burmester
ba6c2d5409 feat: sort by button 2024-08-17 19:11:29 +02:00
Fredrik Burmester
73b266adb4 feat: blurhash 2024-08-17 19:11:16 +02:00
Fredrik Burmester
e0ca83ae1f chore 2024-08-17 13:15:30 +02:00
Fredrik Burmester
4a17a00f81 feat: more media list stuff 2024-08-17 13:15:25 +02:00
Fredrik Burmester
6bfc0c72d1 fix: refactor 2024-08-17 13:14:59 +02:00
Fredrik Burmester
26050f7179 feat: library page 2024-08-17 13:14:33 +02:00
Fredrik Burmester
d39e3aeb80 fix: refactor 2024-08-16 19:56:40 +02:00
Fredrik Burmester
d15d02d61d fix: refactor 2024-08-16 19:11:06 +02:00
Fredrik Burmester
349fc1f21e fix: refactor 2024-08-16 19:06:06 +02:00
Fredrik Burmester
83131cabe1 fix: device profile 2024-08-16 18:52:16 +02:00
Fredrik Burmester
4aec9d350d fix: refactor file layout 2024-08-16 18:51:59 +02:00
Fredrik Burmester
a493f0cad2 fix: force direct play 2024-08-16 18:51:27 +02:00
Fredrik Burmester
329072a7a8 fix: better error handling 2024-08-16 18:51:11 +02:00
Fredrik Burmester
d2ff29a370 fix: more error info 2024-08-16 18:51:01 +02:00
Fredrik Burmester
8f4cb55050 fix: new device profiles 2024-08-16 18:50:45 +02:00
Fredrik Burmester
2c0df550f6 fix: more settings 2024-08-16 18:50:34 +02:00
Fredrik Burmester
a7533bbee0 fix: change profile 2024-08-16 18:50:05 +02:00
Fredrik Burmester
9ec37e26bf fix: poster 2024-08-16 18:49:45 +02:00
Fredrik Burmester
648df75f39 fix: stale time 2024-08-16 18:49:34 +02:00
Fredrik Burmester
84d7983df7 chore 2024-08-16 18:49:15 +02:00
Fredrik Burmester
1f0ff1594b wip: refactoring file structure 2024-08-15 21:08:35 +02:00
Fredrik Burmester
d0baf56fd8 feat: formatting function 2024-08-15 21:08:03 +02:00
Fredrik Burmester
e9182dc4ca chore: remove file 2024-08-15 21:07:51 +02:00
Fredrik Burmester
9817938d44 chore: version 2024-08-15 16:47:05 +02:00
Fredrik Burmester
42ce8f15af fix: new plugin option 2024-08-15 16:47:00 +02:00
Fredrik Burmester
aa0fbd6fad fix: refactor 2024-08-15 16:46:50 +02:00
Fredrik Burmester
92a260b6f5 fix: header and tab background 2024-08-15 16:46:36 +02:00
Fredrik Burmester
ea95cae685 fix: default values 2024-08-15 10:39:40 +02:00
Fredrik Burmester
17641bb529 fix: link 2024-08-15 10:39:35 +02:00
Fredrik Burmester
a8fef13aa8 fix: cls issues
content layout shift
2024-08-15 10:38:17 +02:00
Fredrik Burmester
f91a37ddc2 fix: link correctly 2024-08-15 10:37:26 +02:00
Fredrik Burmester
c67b0ac6cd fix: refactor home screen stack 2024-08-15 10:36:55 +02:00
Fredrik Burmester
33eeebd287 chore: versions 2024-08-14 22:19:36 +02:00
Fredrik Burmester
359aa44ad0 feat: refactor home screen 2024-08-14 22:19:32 +02:00
Fredrik Burmester
5929882ba2 fix: app links 2024-08-14 21:30:59 +02:00
Fredrik Burmester
d976c4b7d6 chore 2024-08-14 18:07:35 +02:00
Fredrik Burmester
cd321301ae feat: choose to open full screen immediately
fixes #47
2024-08-14 18:07:31 +02:00
Fredrik Burmester
2a34851fc6 feat: settings 2024-08-14 15:04:31 +02:00
Fredrik Burmester
a76ed9be66 chore 2024-08-14 15:04:20 +02:00
Fredrik Burmester
1d26b57425 chore 2024-08-14 15:04:13 +02:00
Fredrik Burmester
7bb5060f5f fix: join discord link 2024-08-14 14:39:10 +02:00
Fredrik Burmester
ad8bc954c1 feat: download queue 2024-08-14 13:30:43 +02:00
Fredrik Burmester
f87824ec58 chore 2024-08-14 13:30:35 +02:00
Fredrik Burmester
78556e8764 fix: missing or incorrect pts Issues 2024-08-14 11:10:52 +02:00
Fredrik Burmester
3c678add0f fix: limit 2024-08-14 10:47:31 +02:00
Fredrik Burmester
0c98980b1d feat: ratings 2024-08-14 10:46:19 +02:00
Fredrik Burmester
66179a68ea fix: refactor 2024-08-14 10:46:15 +02:00
Fredrik Burmester
fdd07dce3b chore 2024-08-14 10:10:45 +02:00
Fredrik Burmester
0dc32d58cf chore 2024-08-14 10:10:15 +02:00
Fredrik Burmester
e56c3e5c97 fix: error message 2024-08-14 10:10:07 +02:00
Fredrik Burmester
bd8bf8349f fix: move cast button + ask user which device to play on 2024-08-14 10:09:59 +02:00
Fredrik Burmester
ede390e74b fix: design 2024-08-14 10:09:20 +02:00
Fredrik Burmester
0eca453c9a fix: try to improve download speed 2024-08-14 08:59:43 +02:00
Fredrik Burmester
65838034b6 fix: design 2024-08-14 08:59:25 +02:00
Fredrik Burmester
e715b3daa4 fix: design 2024-08-14 08:59:21 +02:00
Fredrik Burmester
37b7fc1c20 fix: report pause playback on pause 2024-08-14 08:59:17 +02:00
Fredrik Burmester
9ee30ff1ce feat: more bitrate options 2024-08-14 08:58:53 +02:00
Fredrik Burmester
026a286ebf fix: re-fetch hls stream url on sub change 2024-08-14 08:58:42 +02:00
Fredrik Burmester
e522e1dcc0 chore: deps 2024-08-14 08:13:29 +02:00
Fredrik Burmester
a80e065cdb fix: formatting of specials season number 2024-08-14 08:13:14 +02:00
Fredrik Burmester
f4f2d37aea fix: add headers to hls stream url 2024-08-14 08:13:02 +02:00
Fredrik Burmester
e65ed3db0e fix: text 2024-08-13 21:00:33 +02:00
Fredrik Burmester
cb9dfe2c83 feat: app store link 2024-08-13 21:00:08 +02:00
Fredrik Burmester
bc4b07c76b fix: padding 2024-08-13 20:29:40 +02:00
Fredrik Burmester
150eb1809f feat: prev/next episode buttons 2024-08-13 20:26:02 +02:00
Fredrik Burmester
8afe7dc5e4 chore: version 2024-08-13 20:25:50 +02:00
Fredrik Burmester
855e00a676 fix: refactor 2024-08-13 20:25:30 +02:00
Fredrik Burmester
5289c0519f chore: update readme 2024-08-13 16:43:58 +02:00
Fredrik Burmester
4b1eb2218f fix: invalid config 2024-08-13 16:43:50 +02:00
Fredrik Burmester
a99e7b950e fix: pip 2024-08-13 16:21:56 +02:00
Fredrik Burmester
51fc2a0edb chore: versions 2024-08-13 16:02:47 +02:00
Fredrik Burmester
3a13503d1d fix: enable background playback 2024-08-13 16:01:52 +02:00
Fredrik Burmester
2fdf90ab4b fix: enable background play 2024-08-13 16:00:50 +02:00
Fredrik Burmester
6fed0c1c77 fix: change colors 2024-08-13 16:00:43 +02:00
Fredrik Burmester
ee7ff3444e chore: todo 2024-08-13 16:00:39 +02:00
Fredrik Burmester
dec175a300 fix: route to download page 2024-08-13 16:00:26 +02:00
Fredrik Burmester
27099d3184 fix: improve pause/play logic 2024-08-13 16:00:07 +02:00
Fredrik Burmester
bfad77dd7a fix: change color to purple 2024-08-13 15:59:34 +02:00
Fredrik Burmester
74a33f8f82 fix: update download list when a download is finished 2024-08-13 15:59:27 +02:00
Fredrik Burmester
75de878618 fix: show loader for videos but not music 2024-08-13 14:41:37 +02:00
Fredrik Burmester
9628285701 fix: remove parsing of the url 2024-08-13 14:00:18 +02:00
Fredrik Burmester
b206be6bcf chore: version 2024-08-13 13:36:04 +02:00
Fredrik Burmester
656d4ba46b Merge branch 'feat/audio-select' 2024-08-13 13:27:09 +02:00
Fredrik Burmester
b1025c81ae Merge branch 'master' of https://github.com/fredrikburmester/streamyfin 2024-08-13 13:26:33 +02:00
Fredrik Burmester
b05b43c12e feat: poster 2024-08-13 13:26:31 +02:00
Fredrik Burmester
11f9d0fe33 feat: support audio streams 2024-08-13 13:26:27 +02:00
Fredrik Burmester
0498f2e718 chore: version 2024-08-13 11:29:40 +02:00
Fredrik Burmester
077f99fd46 fix: display any collection
fixes #21
2024-08-13 11:29:33 +02:00
Fredrik Burmester
3e433afd4d Update issue templates 2024-08-13 10:13:41 +02:00
Fredrik Burmester
3e1fd5a0ad chore: deps & versions 2024-08-13 10:00:04 +02:00
Fredrik Burmester
0ae8a0a58c fix: change text to remove the word Jellyfin
Because apple denied my app because of it
2024-08-13 09:59:55 +02:00
Fredrik Burmester
34d9392a8b feat: enable screen rotation 2024-08-13 09:59:25 +02:00
Fredrik Burmester
1b463382c5 chore 2024-08-13 09:15:23 +02:00
Fredrik Burmester
4b94bd33ce chore: version 2024-08-13 08:54:16 +02:00
Fredrik Burmester
315d9cbc63 fix: Support for unsecure plaintext authentication (HTTP) logins 2024-08-13 08:54:12 +02:00
Fredrik Burmester
d939f7c9e3 chore 2024-08-13 08:53:59 +02:00
Fredrik Burmester
4d5e544fb0 chore 2024-08-13 08:53:55 +02:00
Fredrik Burmester
5e17f2ac88 fix: check for google play services before chromecast 2024-08-13 08:53:47 +02:00
Fredrik Burmester
74fa279f8d fix: wrong user agent
fixes #14
2024-08-12 22:52:52 +02:00
Fredrik Burmester
4382e585fe fix: typing indicator on android
fixes #15
2024-08-12 22:50:50 +02:00
Fredrik Burmester
a9486c57d2 chore: tipjar 2024-08-12 22:25:04 +02:00
Fredrik Burmester
cc72186a80 feat: audio and subtitle picker 2024-08-12 22:24:51 +02:00
Fredrik Burmester
da9ac3efde fix: download instructions 2024-08-12 20:51:53 +02:00
Fredrik Burmester
7bab4a78bc chore: version 2024-08-12 20:48:09 +02:00
Fredrik Burmester
65837cd303 Merge branch 'master' into feat/audio-select 2024-08-12 20:45:49 +02:00
Fredrik Burmester
5f323d5132 chore: version 2024-08-12 20:45:03 +02:00
Fredrik Burmester
18152b9d5b fix: casting should now work 2024-08-12 20:44:57 +02:00
Fredrik Burmester
d5ee79d740 wip 2024-08-12 19:38:17 +02:00
Fredrik Burmester
040ef3b79a Merge branch 'master' into feat/audio-select 2024-08-12 19:26:48 +02:00
Fredrik Burmester
6b69250ecb fix: splash screen background color 2024-08-12 19:26:39 +02:00
Fredrik Burmester
07c0f81f36 Merge branch 'master' into feat/audio-select 2024-08-12 19:17:52 +02:00
Fredrik Burmester
89a992e7c1 chore: versions 2024-08-12 19:10:08 +02:00
Fredrik Burmester
a62e5d24da wip 2024-08-12 19:09:56 +02:00
397 changed files with 41953 additions and 4565 deletions

1
.env.development Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_WRITE_DEBUG=1

1
.env.production Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_WRITE_DEBUG=0

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text

63
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug]: "
labels:
- ["❌ bug"]
projects:
- ["streamyfin/3"]
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduction steps
description: "How do you trigger this bug? Please walk us through it step by step."
placeholder: |
1.
2.
3.
...
validations:
required: true
- type: textarea
id: device
attributes:
label: Which device and operating system are you using?
description: e.g. iPhone 15, iOS 18.1.1
validations:
required: true
- type: dropdown
id: version
attributes:
label: Version
description: What version of Streamyfin are you running?
options:
- 0.28.0
- 0.27.0
- 0.26.1
- 0.26.0
- 0.25.0
- 0.24.0
- 0.23.0
- 0.22.0
- 0.21.0
- older
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: If applicable, please add screenshots to help explain your problem.
You can drag and drop images here or paste them directly into the comment box.

View File

@@ -0,0 +1,15 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: '✨ enhancement'
assignees: ''
projects:
- streamyfin/3
---
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

80
.github/workflows/build-android.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: 🤖 Android APK Build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
pull_request:
branches: [develop, master]
push:
branches: [develop, master]
jobs:
build:
runs-on: ubuntu-24.04
name: 🏗️ Build Android APK
permissions:
contents: read
steps:
- name: 📥 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
show-progress: false
submodules: recursive
fetch-depth: 0
- name: 🍞 Setup Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
# @todo: update to 1.x once this is fixed: https://github.com/streamyfin/streamyfin/pull/690#discussion_r2089749689
bun-version: '1.2.13'
- name: ☕ Setup JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'
- name: 💾 Cache Bun dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-cache-
- name: 📦 Install dependencies
run: |
bun install --frozen-lockfile
bun run submodule-reload
- name: 💾 Cache Android dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: |
android/.gradle
key: ${{ runner.os }}-android-deps-${{ hashFiles('android/**/build.gradle') }}
restore-keys: |
${{ runner.os }}-android-deps-
- name: 🛠️ Generate project files
run: bun run prebuild
- name: 🚀 Build APK via Bun
run: bun run build:android:local
- name: 📅 Set date tag
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload APK artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: streamyfin-apk-${{ github.sha }}-${{ env.DATE_TAG }}
path: |
android/app/build/outputs/apk/release/*.apk
android/app/build/outputs/bundle/release/*.aab
retention-days: 7

85
.github/workflows/build-ios.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: 🤖 iOS IPA Build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
# push:
# branches: [develop]
jobs:
build:
runs-on: macos-15
name: 🏗️ Build iOS IPA
permissions:
contents: read
steps:
- name: 📥 Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
show-progress: false
submodules: recursive
fetch-depth: 0
- name: 🍞 Setup Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
# @todo: update to 1.x once this is fixed: https://github.com/streamyfin/streamyfin/pull/690#discussion_r2089749689
bun-version: '1.2.13'
- name: 💾 Cache Bun dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-cache-
- name: 📦 Install & Prepare
run: |
bun i && bun run submodule-reload
npx expo prebuild
- name: 🏗️ Build iOS app
uses: sparkfabrik/ios-build-action@be021d9f600b104d199a500db7ba479149a6b257 # v2.3.2
with:
upload-to-testflight: false
increment-build-number: false
build-pods: true
pods-path: "ios/Podfile"
configuration: Release
# Change later to app-store if wanted
export-method: appstore
#export-method: ad-hoc
workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/"
project-path: "ios/Streamyfin.xcodeproj"
scheme: Streamyfin
apple-key-id: ${{ secrets.APPLE_KEY_ID }}
apple-key-issuer-id: ${{ secrets.APPLE_KEY_ISSUER_ID }}
apple-key-content: ${{ secrets.APPLE_KEY_CONTENT }}
team-id: ${{ secrets.TEAM_ID }}
team-name: ${{ secrets.TEAM_NAME }}
#match-password: ${{ secrets.MATCH_PASSWORD }}
#match-git-url: ${{ secrets.MATCH_GIT_URL }}
#match-git-basic-authorization: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
#match-build-type: "appstore"
#browserstack-upload: true
#browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }}
#browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
#fastlane-env: stage
ios-app-id: com.stetsed.teststreamyfin
output-path: build-${{ github.sha }}.ipa
- name: 📅 Set date tag
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload IPA artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: streamyfin-ipa-${{ github.sha }}-${{ env.DATE_TAG }}
path: build-*.ipa
retention-days: 7

47
.github/workflows/check-lockfile.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: 🔒 Lockfile Consistency Check
on:
pull_request:
branches: [develop, master]
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-lockfile:
name: 🔍 Check bun.lock and package.json consistency
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: 📥 Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
show-progress: false
submodules: recursive
fetch-depth: 0
- name: 🍞 Setup Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
# @todo: update to 1.x once this is fixed: https://github.com/streamyfin/streamyfin/pull/690#discussion_r2089749689
with:
bun-version: '1.2.13'
- name: 💾 Cache Bun dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
- name: 🛡️ Verify lockfile consistency
run: |
set -euxo pipefail
echo "➡️ Checking for discrepancies between bun.lock and package.json..."
bun install --frozen-lockfile --dry-run --ignore-scripts
echo "✅ Lockfile is consistent with package.json!"

43
.github/workflows/ci-codeql.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: 🛡️ CodeQL Analysis
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
schedule:
- cron: '24 2 * * *'
jobs:
analyze:
name: 🔎 Analyze with CodeQL
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript-typescript' ]
steps:
- name: 📥 Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
show-progress: false
fetch-depth: 0
- name: 🏁 Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
- name: 🧪 Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18

24
.github/workflows/conflict.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: 🏷🔀Merge Conflict Labeler
on:
push:
branches: [develop]
pull_request_target:
branches: [develop]
types: [synchronize]
jobs:
label:
name: 🏷️ Labeling Merge Conflicts
runs-on: ubuntu-24.04
if: ${{ github.repository == 'streamyfin/streamyfin' }}
permissions:
contents: read
pull-requests: write
steps:
- name: 🚩 Apply merge conflict label
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
with:
dirtyLabel: 'merge-conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: '${{ secrets.GITHUB_TOKEN }}'

96
.github/workflows/linting.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: 🚦 Security & Quality Gate
on:
pull_request_target:
types: [opened, edited, synchronize, reopened]
branches: [develop, master]
workflow_dispatch:
permissions:
contents: read
jobs:
validate_pr_title:
name: "📝 Validate PR Title"
runs-on: ubuntu-24.04
permissions:
pull-requests: write
contents: read
steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/).
**Error details:**
```
${{ steps.lint_pr_title.outputs.error_message }}
```
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2
with:
header: pr-title-lint-error
delete: true
dependency-review:
name: 🔍 Vulnerable Dependencies
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Dependency Review
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
code_quality:
name: "🔍 Lint & Test (${{ matrix.command }})"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- "lint"
- "check"
steps:
- name: "📥 Checkout PR code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
submodules: recursive
fetch-depth: 0
- name: "🟢 Setup Node.js"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '20.x'
- name: "🍞 Setup Bun"
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
# @todo: update to 1.x once this is fixed: https://github.com/streamyfin/streamyfin/pull/690#discussion_r2089749689
bun-version: '1.2.13'
- name: "📦 Install dependencies"
run: bun install --frozen-lockfile
- name: "🚨 Run ${{ matrix.command }}"
run: bun run ${{ matrix.command }}

23
.github/workflows/notification.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: 🛎️ Discord Pull Request Notification
on:
pull_request:
types: [opened, reopened]
branches: [develop]
jobs:
notify:
runs-on: ubuntu-24.04
steps:
- name: 🛎️ Notify Discord
uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9 # 0.3.2
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
DISCORD_AVATAR: https://avatars.githubusercontent.com/u/193271640
with:
args: |
📢 New Pull Request in **${{ github.repository }}**
**Title:** ${{ github.event.pull_request.title }}
**By:** ${{ github.event.pull_request.user.login }}
**Branch:** ${{ github.event.pull_request.head.ref }}
🔗 ${{ github.event.pull_request.html_url }}

49
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: 🕒 Handle Stale Issues
on:
schedule:
# Runs daily at 1:30 AM UTC (3:30 AM CEST - France time)
- cron: "30 1 * * *"
jobs:
stale-issues:
name: 🗑️ Cleanup Stale Issues
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- name: 🔄 Mark/Close Stale Issues
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
# Global settings
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 500 # Increase if you have >1000 issues
log-level: debug
# Issue configuration
days-before-issue-stale: 90
days-before-issue-close: 7
stale-issue-label: "stale"
exempt-issue-labels: "Roadmap v1,help needed,enhancement"
# Notifications messages
stale-issue-message: |
⏳ This issue has been automatically marked as **stale** because it has had no activity for 90 days.
**Next steps:**
- If this is still relevant, add a comment to keep it open
- Otherwise, it will be closed in 7 days
Thank you for your contributions! 🙌
close-issue-message: |
🚮 This issue has been automatically closed due to inactivity (7 days since being marked stale).
**Need to reopen?**
Click "Reopen" and add a comment explaining why this should stay open.
# Disable PR handling
days-before-pr-stale: -1
days-before-pr-close: -1

23
.gitignore vendored
View File

@@ -9,6 +9,8 @@ npm-debug.*
*.mobileprovision
*.orig.*
web-build/
modules/vlc-player/android/build
modules/vlc-player/android/.gradle
# macOS
.DS_Store
@@ -17,13 +19,30 @@ expo-env.d.ts
Streamyfin.app
build-*
*.mp4
build-*
Streamyfin.app
package-lock.json
/ios
/android
/iostv
/iosmobile
/androidmobile
/androidtv
modules/player/android
pc-api-7079014811501811218-719-3b9f15aeccf8.json
credentials.json
*.apk
*.ipa
.continuerc.json
.vscode/
.idea/
.ruby-lsp
modules/hls-downloader/android/build
streamyfin-4fec1-firebase-adminsdk.json
.env
.env.local
*.aab

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "utils/jellyseerr"]
path = utils/jellyseerr
url = https://github.com/herrrta/jellyseerr
branch = models

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
lint-staged

20
.vscode/settings.json vendored
View File

@@ -1,12 +1,24 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"prettier.printWidth": 120,
"[swift]": {
"editor.defaultFormatter": "sswg.swift-lang"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}
}

6
Makefile Normal file
View File

@@ -0,0 +1,6 @@
e2e:
maestro start-device --platform android
maestro test login.yaml
e2e-setup:
curl -fsSL "https://get.maestro.mobile.dev" | bash

206
README.md
View File

@@ -1,39 +1,74 @@
# 📺 Streamyfin
Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Expo. If you're looking for an alternative to other Jellyfin clients, we hope you'll find Streamyfin to be a useful addition to your media streaming toolbox.
<a href="https://www.buymeacoffee.com/fredrikbur3" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
<div style="display: flex; flex-direction: row; gap: 5px">
<img width=100 src="./assets/images/screenshots/1.jpg" />
<img width=100 src="./assets/images/screenshots/3.jpg" />
<img width=100 src="./assets/images/screenshots/4.jpg" />
<img width=100 src="./assets/images/screenshots/5.jpg" />
<img width=100 src="./assets/images/screenshots/7.jpg" />
Welcome to Streamyfin, a simple and user-friendly Jellyfin video streaming client built with Expo. If you're looking for an alternative to other Jellyfin clients, we hope you'll find Streamyfin to be a useful addition to your media streaming toolbox.
<div style="display: flex; flex-direction: row; gap: 8px">
<img width=150 src="./assets/images/screenshots/screenshot1.png" />
<img width=150 src="./assets/images/screenshots/screenshot3.png" />
<img width=150 src="./assets/images/screenshots/screenshot2.png" />
<img width=159 src="./assets/images/jellyseerr.PNG"/>
</div>
## 🌟 Features
- 🔗 Connect to your Jellyfin instance: Easily link your Jellyfin server and access your media library.
- 📱 Native video player: Playback with the platform native video player. With support for subtitles, playback speed control, and more.
- 📥 Download media (Experimental): Save your media locally and watch it offline.
- 📡 Chromecast media (Experimental): Cast your media to any Chromecast-enabled device.
- 🚀 **Skip Intro / Credits Support**
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
- 📡 **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device.
- 📡 **Settings management** (Experimental): Manage app settings for all your users with a JF plugin.
- 🤖 **Jellyseerr integration**: Request media directly in the app.
- 👁️ **Sessions View:** View all active sessions currently streaming on your server.
## 🧪 Experimental Features
Streamyfin includes some exciting experimental features like media downloading and Chromecast support. These are still in development, and we appreciate your patience and feedback as we work to improve them.
## 🛠️ Beta testing (iOS/Android)
### Downloading
## TestFlight
Downloading works by using ffmpeg to convert an HLS stream into a video file on the device. This means that you can download and view any file you can stream! The file is converted by Jellyfin on the server in real time as it is downloaded. This means a **bit longer download times** but supports any file that your server can transcode.
<a href="https://testflight.apple.com/join/CWBaAAK2">
<img height=75 alt="Get the beta on TestFlight" src="./assets/Get_the_beta_on_Testflight.svg"/>
</a>
### Chromecast
## Play Store Open Beta
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos, but we're working on adding support for subtitles and other features.
<a href="https://play.google.com/store/apps/details?id=com.fredrikburmester.streamyfin">
<img height=75 alt="Get the beta on Google Play" src="./assets/en_badge_web_generic.png"/>
</a>
### Streamyfin Plugin
The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that holds all settings for the client Streamyfin. This allows you to synchronize settings across all your users, like for example:
- Auto log in to Jellyseerr without the user having to do anything
- Choose the default languages
- Set download method and search provider
- Customize home screen
- And more...
[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
### Jellysearch
[Jellysearch](https://gitlab.com/DomiStyle/jellysearch) now works with Streamyfin! 🚀
> A fast full-text search proxy for Jellyfin. Integrates seamlessly with most Jellyfin clients.
## Roadmap for V1
Check out our [Roadmap](https://github.com/users/fredrikburmester/projects/5) to see what we're working on next. We are always open for feedback and suggestions, so please let us know if you have any ideas or feature requests.
## Get it now
<div style="display: flex; gap: 5px;">
<a href="https://apps.apple.com/app/streamyfin/id6593660679?l=en-GB"><img height=50 alt="Get Streamyfin on App Store" src="./assets/Download_on_the_App_Store_Badge.png"/></a>
<a href="https://play.google.com/store/apps/details?id=com.fredrikburmester.streamyfin"><img height=50 alt="Get the beta on Google Play" src="./assets/Google_Play_Store_badge_EN.svg"/></a>
</div>
Or download the APKs [here on GitHub](https://github.com/streamyfin/streamyfin/releases) for Android.
### Beta testing
To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠🧪-public-beta channel on Discord and I'll know that you have subscribed. This is where I post APKs and IPAs. This won't give automatic access to the TestFlight, however, so you need to send me a DM with the email you use for Apple so that I can manually add you.
**Note**: Everyone who is actively contributing to the source code of Streamyfin will have automatic access to the betas.
## 🚀 Getting Started
@@ -48,30 +83,17 @@ We welcome any help to make Streamyfin better. If you'd like to contribute, plea
### Development info
Add this to AppDelegate.mm:
1. Use node `>20`
2. Install dependencies `bun i && bun run submodule-reload`
3. Make sure you have xcode and/or android studio installed. (follow the guides for expo: https://docs.expo.dev/workflow/android-studio-emulator/)
4. Install BiomeJS extension in VSCode/Your IDE (https://biomejs.dev/)
4. run `npm run prebuild`
5. Create an expo dev build by running `npm run ios` or `npm run android`. This will open a simulator on your computer and run the app.
```
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// @generated begin react-native-google-cast-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-8901be60b982d2ae9c658b1e8c50634d61bb5091
#if __has_include(<GoogleCast/GoogleCast.h>)
...
For the TV version suffix the npm commands with `:tv`.
[GCKCastContext sharedInstance].useDefaultExpandedMediaControls = true;`
#endif
```
Add this to Info.plist:
```
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi network.</string>
```
`npm run prebuild:tv`
`npm run ios:tv or npm run android:tv`
## 📄 License
@@ -89,19 +111,115 @@ Key points of the MPL-2.0:
## 🌐 Connect with Us
Join our Discord: [https://discord.gg/aJvAYeycyY](https://discord.gg/aJvAYeycyY)
If you have questions or need support, feel free to reach out:
- GitHub Issues: Report bugs or request features here.
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
## FAQ
1. Q: Why can't I see my libraries in Streamyfin?
A: Make sure your server is running one of the latest versions and that you have at least one library that isn't audio only.
2. Q: Why can't I see my music library?
A: We don't currently support music and are unlikely to support music in the near future.
## 📝 Credits
Streamyfin is developed by Fredrik Burmester and is not affiliated with Jellyfin. The app is built with Expo, React Native, and other open-source libraries.
Streamyfin is developed by [Fredrik Burmester](https://github.com/fredrikburmester) and is not affiliated with Jellyfin. The app is built with Expo, React Native, and other open-source libraries.
## ✨ Acknowledgements
I'd like to thank the following people and projects for their contributions to Streamyfin:
We would like to thank the Jellyfin team for their great software and awesome support on discord.
Special shoutout to the JF official clients for being an inspiration to ours.
### Core Developers
Thanks to the following contributors for their significant contributions:
<table>
<tr
style="
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
"
>
<td align="center">
<a href="https://github.com/Alexk2309">
<img src="https://github.com/Alexk2309.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@Alexk2309</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/herrrta">
<img src="https://github.com/herrrta.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@herrrta</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lostb1t">
<img src="https://github.com/lostb1t.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@lostb1t</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Simon-Eklundh">
<img src="https://github.com/Simon-Eklundh.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@Simon-Eklundh</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/topiga">
<img src="https://github.com/topiga.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@topiga</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/simoncaron">
<img src="https://github.com/simoncaron.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@simoncaron</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jakequade">
<img src="https://github.com/jakequade.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@jakequade</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Ryan0204">
<img src="https://github.com/Ryan0204.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@Ryan0204</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/retardgerman">
<img src="https://github.com/retardgerman.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@retardgerman</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/whoopsi-daisy">
<img src="https://github.com/whoopsi-daisy.png?size=80" width="80" style="border-radius: 50%;" />
<br /><sub><b>@whoopsi-daisy</b></sub>
</a>
</td>
</tr>
</table>
And all other developers who have contributed to Streamyfin, thank you for your contributions.
I'd also like to thank the following people and projects for their contributions to Streamyfin:
- [Reiverr](https://github.com/aleksilassila/reiverr) for great help with understanding the Jellyfin API.
- [Jellyfin TS SDK](https://github.com/jellyfin/jellyfin-sdk-typescript) for the TypeScript SDK.
- [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) for enabling API integration with their project.
- The Jellyfin devs for always being helpful in the Discord.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=streamyfin/streamyfin&type=Date)](https://star-history.com/#streamyfin/streamyfin&Date)

14
app.config.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = ({ config }) => {
if (process.env.EXPO_TV !== "1") {
config.plugins.push([
"react-native-google-cast",
{ useDefaultExpandedMediaControls: true },
]);
}
return {
android: {
googleServicesFile: process.env.GOOGLE_SERVICES_JSON,
},
...config,
};
};

122
app.json
View File

@@ -2,63 +2,63 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.2.0",
"orientation": "portrait",
"version": "0.28.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
"userInterfaceStyle": "dark",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"jsEngine": "hermes",
"assetBundlePatterns": ["**/*"],
"ios": {
"requireFullScreen": true,
"infoPlist": {
"NSCameraUsageDescription": "The app needs access to your camera to scan barcodes.",
"NSMicrophoneUsageDescription": "The app needs access to your microphone."
"NSMicrophoneUsageDescription": "The app needs access to your microphone.",
"UIBackgroundModes": ["audio", "fetch"],
"NSLocalNetworkUsageDescription": "The app needs access to your local network to connect to your Jellyfin server.",
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
},
"UISupportsTrueScreenSizeOnMac": true,
"UIFileSharingEnabled": true,
"LSSupportsOpeningDocumentsInPlace": true
},
"config": {
"usesNonExemptEncryption": false
},
"supportsTablet": true,
"bundleIdentifier": "com.fredrikburmester.streamyfin"
"bundleIdentifier": "com.fredrikburmester.streamyfin",
"icon": {
"dark": "./assets/images/icon-plain.png",
"light": "./assets/images/icon-ios-light.png",
"tinted": "./assets/images/icon-ios-tinted.png"
}
},
"android": {
"jsEngine": "jsc",
"androidNavigationBar": {
"visible": true,
"barStyle": "dark-content",
"backgroundColor": "#000000"
},
"jsEngine": "hermes",
"versionCode": 55,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon.png"
"foregroundImage": "./assets/images/icon-plain.png",
"monochromeImage": "./assets/images/icon-mono.png",
"backgroundColor": "#464646"
},
"package": "com.fredrikburmester.streamyfin",
"permissions": [
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
"android.permission.WRITE_SETTINGS"
],
"versionCode": 6
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
"googleServicesFile": "./google-services.json"
},
"plugins": [
"@react-native-tvos/config-tv",
"expo-router",
"expo-font",
"react-native-compressor",
"@config-plugins/ffmpeg-kit-react-native",
[
"react-native-google-cast",
{
"useDefaultExpandedMediaControls": true
}
],
[
"react-native-video",
{
"enableNotificationControls": true,
"enableBackgroundAudio": true,
"androidExtensions": {
"useExoplayerRtsp": false,
"useExoplayerSmoothStreaming": false,
@@ -71,17 +71,68 @@
"expo-build-properties",
{
"ios": {
"deploymentTarget": "14.0"
"deploymentTarget": "15.6",
"useFrameworks": "static"
},
"android": {
"compileSdkVersion": 35,
"targetSdkVersion": 35,
"buildToolsVersion": "35.0.0",
"kotlinVersion": "2.0.21",
"minSdkVersion": 24,
"usesCleartextTraffic": true,
"packagingOptions": {
"jniLibs": {
"useLegacyPackaging": true
}
}
},
"useAndroidX": true,
"enableJetifier": true
}
}
],
[
"expo-screen-orientation",
{
"initialOrientation": "DEFAULT"
}
],
[
"expo-sensors",
{
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
}
],
"expo-localization",
"expo-asset",
[
"react-native-edge-to-edge",
{
"android": {
"parentTheme": "Material3"
}
}
],
["react-native-bottom-tabs"],
["./plugins/withChangeNativeAndroidTextToWhite.js"],
["./plugins/withAndroidManifest.js"],
["./plugins/withTrustLocalCerts.js"],
["./plugins/withGradleProperties.js"],
["./plugins/withRNBackgroundDownloader.js"],
[
"expo-splash-screen",
{
"backgroundColor": "#2e2e2e",
"image": "./assets/images/StreamyFinFinal.png",
"imageWidth": 100
}
],
[
"expo-notifications",
{
"icon": "./assets/images/notification.png",
"color": "#9333EA"
}
]
],
"experiments": {
@@ -95,12 +146,13 @@
"projectId": "e79219d1-797f-4fbe-9fa1-cfd360690a68"
}
},
"owner": "fredrikburmester",
"owner": "streamyfin",
"runtimeVersion": {
"policy": "appVersion"
},
"updates": {
"url": "https://u.expo.dev/e79219d1-797f-4fbe-9fa1-cfd360690a68"
}
},
"newArchEnabled": false
}
}

View File

@@ -0,0 +1,22 @@
import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
export default function CustomMenuLayout() {
const { t } = useTranslation();
return (
<Stack>
<Stack.Screen
name='index'
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: t("tabs.custom_links"),
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
</Stack>
);
}

View File

@@ -0,0 +1,87 @@
import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/list/ListItem";
import { apiAtom } from "@/providers/JellyfinProvider";
import Ionicons from "@expo/vector-icons/Ionicons";
import { useAtom } from "jotai/index";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { FlatList, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const WebBrowser = !Platform.isTV ? require("expo-web-browser") : null;
export interface MenuLink {
name: string;
url: string;
icon: string;
}
export default function menuLinks() {
const [api] = useAtom(apiAtom);
const insets = useSafeAreaInsets();
const [menuLinks, setMenuLinks] = useState<MenuLink[]>([]);
const { t } = useTranslation();
const getMenuLinks = useCallback(async () => {
try {
const response = await api?.axiosInstance.get(
`${api?.basePath}/web/config.json`,
);
const config = response?.data;
if (!config && !Object.hasOwn(config, "menuLinks")) {
console.error("Menu links not found");
return;
}
setMenuLinks(config?.menuLinks as MenuLink[]);
} catch (error) {
console.error("Failed to retrieve config:", error);
}
}, [api]);
useEffect(() => {
getMenuLinks();
}, []);
return (
<FlatList
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingTop: 10,
paddingLeft: insets.left,
paddingRight: insets.right,
}}
data={menuLinks}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => {
if (!Platform.isTV) {
WebBrowser.openBrowserAsync(item.url);
}
}}
>
<ListItem
title={item.name}
iconAfter={<Ionicons name='link' size={24} color='white' />}
/>
</TouchableOpacity>
)}
ItemSeparatorComponent={() => (
<View
style={{
width: 10,
height: 10,
}}
/>
)}
ListEmptyComponent={
<View className='flex flex-col items-center justify-center h-full'>
<Text className='font-bold text-xl text-neutral-500'>
{t("custom_links.no_links")}
</Text>
</View>
}
/>
);
}

View File

@@ -0,0 +1,29 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
export default function SearchLayout() {
const { t } = useTranslation();
return (
<Stack>
<Stack.Screen
name='index'
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: t("tabs.favorites"),
headerLargeStyle: {
backgroundColor: "black",
},
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}
</Stack>
);
}

View File

@@ -0,0 +1,36 @@
import { Favorites } from "@/components/home/Favorites";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import React, { useCallback, useState } from "react";
import { RefreshControl, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function favorites() {
const invalidateCache = useInvalidatePlaybackProgressCache();
const [loading, setLoading] = useState(false);
const refetch = useCallback(async () => {
setLoading(true);
await invalidateCache();
setLoading(false);
}, []);
const insets = useSafeAreaInsets();
return (
<ScrollView
nestedScrollEnabled
contentInsetAdjustmentBehavior='automatic'
refreshControl={
<RefreshControl refreshing={loading} onRefresh={refetch} />
}
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
>
<View className='my-4'>
<Favorites />
</View>
</ScrollView>
);
}

View File

@@ -0,0 +1,155 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Feather, Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native";
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { userAtom } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
export default function IndexLayout() {
const router = useRouter();
const [user] = useAtom(userAtom);
const { t } = useTranslation();
return (
<Stack>
<Stack.Screen
name='index'
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: t("tabs.home"),
headerBlurEffect: "prominent",
headerLargeStyle: {
backgroundColor: "black",
},
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
headerRight: () => (
<View className='flex flex-row items-center space-x-2'>
{!Platform.isTV && (
<>
<Chromecast.Chromecast />
{user?.Policy?.IsAdministrator && <SessionsButton />}
<SettingsButton />
</>
)}
</View>
),
}}
/>
<Stack.Screen
name='downloads/index'
options={{
title: t("home.downloads.downloads_title"),
}}
/>
<Stack.Screen
name='downloads/[seriesId]'
options={{
title: t("home.downloads.tvseries"),
}}
/>
<Stack.Screen
name='sessions/index'
options={{
title: t("home.sessions.title"),
}}
/>
<Stack.Screen
name='settings'
options={{
title: t("home.settings.settings_title"),
}}
/>
<Stack.Screen
name='settings/optimized-server/page'
options={{
title: "",
}}
/>
<Stack.Screen
name='settings/marlin-search/page'
options={{
title: "",
}}
/>
<Stack.Screen
name='settings/jellyseerr/page'
options={{
title: "",
}}
/>
<Stack.Screen
name='settings/hide-libraries/page'
options={{
title: "",
}}
/>
<Stack.Screen
name='settings/logs/page'
options={{
title: "",
}}
/>
<Stack.Screen
name='intro/page'
options={{
headerShown: false,
title: "",
presentation: "modal",
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}
<Stack.Screen
name='collections/[collectionId]'
options={{
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
</Stack>
);
}
const SettingsButton = () => {
const router = useRouter();
return (
<TouchableOpacity
onPress={() => {
router.push("/(auth)/settings");
}}
>
<Feather name='settings' color={"white"} size={22} />
</TouchableOpacity>
);
};
const SessionsButton = () => {
const router = useRouter();
const { sessions = [] } = useSessions({} as useSessionsProps);
return (
<TouchableOpacity
onPress={() => {
router.push("/(auth)/sessions");
}}
>
<View className='mr-4'>
<Ionicons
name='play-circle'
color={sessions.length === 0 ? "white" : "#9333ea"}
size={25}
/>
</View>
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,132 @@
import { Text } from "@/components/common/Text";
import { EpisodeCard } from "@/components/downloads/EpisodeCard";
import {
SeasonDropdown,
type SeasonIndexState,
} from "@/components/series/SeasonDropdown";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
export default function page() {
const navigation = useNavigation();
const local = useLocalSearchParams();
const { seriesId, episodeSeasonIndex } = local as {
seriesId: string;
episodeSeasonIndex: number | string | undefined;
};
const [seasonIndexState, setSeasonIndexState] = useState<SeasonIndexState>(
{},
);
const { downloadedFiles, deleteItems } = useDownload();
const series = useMemo(() => {
try {
return (
downloadedFiles
?.filter((f) => f.item.SeriesId === seriesId)
?.sort(
(a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!,
) || []
);
} catch {
return [];
}
}, [downloadedFiles]);
const seasonIndex =
seasonIndexState[series?.[0]?.item?.ParentId ?? ""] ||
episodeSeasonIndex ||
"";
const groupBySeason = useMemo<BaseItemDto[]>(() => {
const seasons: Record<string, BaseItemDto[]> = {};
series?.forEach((episode) => {
if (!seasons[episode.item.ParentIndexNumber!]) {
seasons[episode.item.ParentIndexNumber!] = [];
}
seasons[episode.item.ParentIndexNumber!].push(episode.item);
});
return (
seasons[seasonIndex]?.sort((a, b) => a.IndexNumber! - b.IndexNumber!) ??
[]
);
}, [series, seasonIndex]);
const initialSeasonIndex = useMemo(
() =>
Object.values(groupBySeason)?.[0]?.ParentIndexNumber ??
series?.[0]?.item?.ParentIndexNumber,
[groupBySeason],
);
useEffect(() => {
if (series.length > 0) {
navigation.setOptions({
title: series[0].item.SeriesName,
});
} else {
storage.delete(seriesId);
router.back();
}
}, [series]);
const deleteSeries = useCallback(() => {
Alert.alert(
"Delete season",
"Are you sure you want to delete the entire season?",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
onPress: () => deleteItems(groupBySeason),
style: "destructive",
},
],
);
}, [groupBySeason]);
return (
<View className='flex-1'>
{series.length > 0 && (
<View className='flex flex-row items-center justify-start my-2 px-4'>
<SeasonDropdown
item={series[0].item}
seasons={series.map((s) => s.item)}
state={seasonIndexState}
initialSeasonIndex={initialSeasonIndex!}
onSelect={(season) => {
setSeasonIndexState((prev) => ({
...prev,
[series[0].item.ParentId ?? ""]: season.ParentIndexNumber,
}));
}}
/>
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center ml-2'>
<Text className='text-xs font-bold'>{groupBySeason.length}</Text>
</View>
<View className='bg-neutral-800/80 rounded-full h-9 w-9 flex items-center justify-center ml-auto'>
<TouchableOpacity onPress={deleteSeries}>
<Ionicons name='trash' size={20} color='white' />
</TouchableOpacity>
</View>
</View>
)}
<ScrollView key={seasonIndex} className='px-4'>
{groupBySeason.map((episode, index) => (
<EpisodeCard key={index} item={episode} />
))}
</ScrollView>
</View>
);
}

View File

@@ -0,0 +1,271 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ActiveDownloads } from "@/components/downloads/ActiveDownloads";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard";
import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { writeToLog } from "@/utils/log";
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useNavigation, useRouter } from "expo-router";
import { t } from "i18next";
import { useAtom } from "jotai";
import React, { useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
export default function page() {
const navigation = useNavigation();
const { t } = useTranslation();
const [queue, setQueue] = useAtom(queueAtom);
const { removeProcess, downloadedFiles, deleteFileByType } = useDownload();
const router = useRouter();
const [settings] = useSettings();
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const movies = useMemo(() => {
try {
return downloadedFiles?.filter((f) => f.item.Type === "Movie") || [];
} catch {
migration_20241124();
return [];
}
}, [downloadedFiles]);
const groupedBySeries = useMemo(() => {
try {
const episodes = downloadedFiles?.filter(
(f) => f.item.Type === "Episode",
);
const series: { [key: string]: DownloadedItem[] } = {};
episodes?.forEach((e) => {
if (!series[e.item.SeriesName!]) series[e.item.SeriesName!] = [];
series[e.item.SeriesName!].push(e);
});
return Object.values(series);
} catch {
migration_20241124();
return [];
}
}, [downloadedFiles]);
const insets = useSafeAreaInsets();
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={bottomSheetModalRef.current?.present}>
<DownloadSize items={downloadedFiles?.map((f) => f.item) || []} />
</TouchableOpacity>
),
});
}, [downloadedFiles]);
const deleteMovies = () =>
deleteFileByType("Movie")
.then(() =>
toast.success(
t("home.downloads.toasts.deleted_all_movies_successfully"),
),
)
.catch((reason) => {
writeToLog("ERROR", reason);
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
});
const deleteShows = () =>
deleteFileByType("Episode")
.then(() =>
toast.success(
t("home.downloads.toasts.deleted_all_tvseries_successfully"),
),
)
.catch((reason) => {
writeToLog("ERROR", reason);
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
});
const deleteAllMedia = async () =>
await Promise.all([deleteMovies(), deleteShows()]);
return (
<>
<ScrollView
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 100,
}}
>
<View className='py-4'>
<View className='mb-4 flex flex-col space-y-4 px-4'>
{settings?.downloadMethod === DownloadMethod.Remux && (
<View className='bg-neutral-900 p-4 rounded-2xl'>
<Text className='text-lg font-bold'>
{t("home.downloads.queue")}
</Text>
<Text className='text-xs opacity-70 text-red-600'>
{t("home.downloads.queue_hint")}
</Text>
<View className='flex flex-col space-y-2 mt-2'>
{queue.map((q, index) => (
<TouchableOpacity
onPress={() =>
router.push(`/(auth)/items/page?id=${q.item.Id}`)
}
className='relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between'
key={index}
>
<View>
<Text className='font-semibold'>{q.item.Name}</Text>
<Text className='text-xs opacity-50'>
{q.item.Type}
</Text>
</View>
<TouchableOpacity
onPress={() => {
removeProcess(q.id);
setQueue((prev) => {
if (!prev) return [];
return [...prev.filter((i) => i.id !== q.id)];
});
}}
>
<Ionicons name='close' size={24} color='red' />
</TouchableOpacity>
</TouchableOpacity>
))}
</View>
{queue.length === 0 && (
<Text className='opacity-50'>
{t("home.downloads.no_items_in_queue")}
</Text>
)}
</View>
)}
<ActiveDownloads />
</View>
{movies.length > 0 && (
<View className='mb-4'>
<View className='flex flex-row items-center justify-between mb-2 px-4'>
<Text className='text-lg font-bold'>
{t("home.downloads.movies")}
</Text>
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
<Text className='text-xs font-bold'>{movies?.length}</Text>
</View>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className='px-4 flex flex-row'>
{movies?.map((item) => (
<View className='mb-2 last:mb-0' key={item.item.Id}>
<MovieCard item={item.item} />
</View>
))}
</View>
</ScrollView>
</View>
)}
{groupedBySeries.length > 0 && (
<View className='mb-4'>
<View className='flex flex-row items-center justify-between mb-2 px-4'>
<Text className='text-lg font-bold'>
{t("home.downloads.tvseries")}
</Text>
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
<Text className='text-xs font-bold'>
{groupedBySeries?.length}
</Text>
</View>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className='px-4 flex flex-row'>
{groupedBySeries?.map((items) => (
<View
className='mb-2 last:mb-0'
key={items[0].item.SeriesId}
>
<SeriesCard
items={items.map((i) => i.item)}
key={items[0].item.SeriesId}
/>
</View>
))}
</View>
</ScrollView>
</View>
)}
{downloadedFiles?.length === 0 && (
<View className='flex px-4'>
<Text className='opacity-50'>
{t("home.downloads.no_downloaded_items")}
</Text>
</View>
)}
</View>
</ScrollView>
<BottomSheetModal
ref={bottomSheetModalRef}
enableDynamicSizing
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
backdropComponent={(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
)}
>
<BottomSheetView>
<View className='p-4 space-y-4 mb-4'>
<Button color='purple' onPress={deleteMovies}>
{t("home.downloads.delete_all_movies_button")}
</Button>
<Button color='purple' onPress={deleteShows}>
{t("home.downloads.delete_all_tvseries_button")}
</Button>
<Button color='red' onPress={deleteAllMedia}>
{t("home.downloads.delete_all_button")}
</Button>
</View>
</BottomSheetView>
</BottomSheetModal>
</>
);
}
function migration_20241124() {
const router = useRouter();
const { deleteAllFiles } = useDownload();
Alert.alert(
t("home.downloads.new_app_version_requires_re_download"),
t("home.downloads.new_app_version_requires_re_download_description"),
[
{
text: t("home.downloads.back"),
onPress: () => router.back(),
},
{
text: t("home.downloads.delete"),
style: "destructive",
onPress: async () => await deleteAllFiles(),
},
],
);
}

View File

@@ -0,0 +1,5 @@
import { HomeIndex } from "@/components/settings/HomeIndex";
export default function page() {
return <HomeIndex />;
}

View File

@@ -0,0 +1,141 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { storage } from "@/utils/mmkv";
import { Feather, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useFocusEffect, useRouter } from "expo-router";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Linking, TouchableOpacity, View } from "react-native";
export default function page() {
const router = useRouter();
const { t } = useTranslation();
useFocusEffect(
useCallback(() => {
storage.set("hasShownIntro", true);
}, []),
);
return (
<View className='bg-neutral-900 h-full py-16 px-4 space-y-8'>
<View>
<Text className='text-3xl font-bold text-center mb-2'>
{t("home.intro.welcome_to_streamyfin")}
</Text>
<Text className='text-center'>
{t("home.intro.a_free_and_open_source_client_for_jellyfin")}
</Text>
</View>
<View>
<Text className='text-lg font-bold'>
{t("home.intro.features_title")}
</Text>
<Text className='text-xs'>{t("home.intro.features_description")}</Text>
<View className='flex flex-row items-center mt-4'>
<Image
source={require("@/assets/icons/jellyseerr-logo.svg")}
style={{
width: 50,
height: 50,
}}
/>
<View className='shrink ml-2'>
<Text className='font-bold mb-1'>Jellyseerr</Text>
<Text className='shrink text-xs'>
{t("home.intro.jellyseerr_feature_description")}
</Text>
</View>
</View>
<View className='flex flex-row items-center mt-4'>
<View
style={{
width: 50,
height: 50,
}}
className='flex items-center justify-center'
>
<Ionicons name='cloud-download-outline' size={32} color='white' />
</View>
<View className='shrink ml-2'>
<Text className='font-bold mb-1'>
{t("home.intro.downloads_feature_title")}
</Text>
<Text className='shrink text-xs'>
{t("home.intro.downloads_feature_description")}
</Text>
</View>
</View>
<View className='flex flex-row items-center mt-4'>
<View
style={{
width: 50,
height: 50,
}}
className='flex items-center justify-center'
>
<Feather name='cast' size={28} color={"white"} />
</View>
<View className='shrink ml-2'>
<Text className='font-bold mb-1'>Chromecast</Text>
<Text className='shrink text-xs'>
{t("home.intro.chromecast_feature_description")}
</Text>
</View>
</View>
<View className='flex flex-row items-center mt-4'>
<View
style={{
width: 50,
height: 50,
}}
className='flex items-center justify-center'
>
<Feather name='settings' size={28} color={"white"} />
</View>
<View className='shrink ml-2'>
<Text className='font-bold mb-1'>
{t("home.intro.centralised_settings_plugin_title")}
</Text>
<Text className='shrink text-xs'>
{t("home.intro.centralised_settings_plugin_description")}{" "}
<Text
className='text-purple-600'
onPress={() => {
Linking.openURL(
"https://github.com/streamyfin/jellyfin-plugin-streamyfin",
);
}}
>
{t("home.intro.read_more")}
</Text>
</Text>
</View>
</View>
</View>
<View>
<Button
onPress={() => {
router.back();
}}
className='mt-4'
>
{t("home.intro.done_button")}
</Button>
<TouchableOpacity
onPress={() => {
router.back();
router.push("/settings");
}}
className='mt-4'
>
<Text className='text-purple-600 text-center'>
{t("home.intro.go_to_settings_button")}
</Text>
</TouchableOpacity>
</View>
</View>
);
}

View File

@@ -0,0 +1,556 @@
import { Badge } from "@/components/Badge";
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import Poster from "@/components/posters/Poster";
import { useInterval } from "@/hooks/useInterval";
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { formatBitrate } from "@/utils/bitrate";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { formatTimeString } from "@/utils/time";
import {
AntDesign,
Entypo,
Ionicons,
MaterialCommunityIcons,
} from "@expo/vector-icons";
import {
HardwareAccelerationType,
type SessionInfoDto,
} from "@jellyfin/sdk/lib/generated-client";
import {
GeneralCommandType,
PlaystateCommand,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { get } from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
export default function page() {
const { sessions, isLoading } = useSessions({} as useSessionsProps);
const { t } = useTranslation();
if (isLoading)
return (
<View className='justify-center items-center h-full'>
<Loader />
</View>
);
if (!sessions || sessions.length === 0)
return (
<View className='h-full w-full flex justify-center items-center'>
<Text className='text-lg text-neutral-500'>
{t("home.sessions.no_active_sessions")}
</Text>
</View>
);
return (
<FlashList
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingTop: 17,
paddingHorizontal: 17,
paddingBottom: 150,
}}
data={sessions}
renderItem={({ item }) => <SessionCard session={item} />}
keyExtractor={(item) => item.Id || ""}
estimatedItemSize={200}
/>
);
}
interface SessionCardProps {
session: SessionInfoDto;
}
const SessionCard = ({ session }: SessionCardProps) => {
const api = useAtomValue(apiAtom);
const [remainingTicks, setRemainingTicks] = useState<number>(0);
const tick = () => {
if (session.PlayState?.IsPaused) return;
setRemainingTicks(remainingTicks - 10000000);
};
const getProgressPercentage = () => {
if (!session.NowPlayingItem || !session.NowPlayingItem.RunTimeTicks) {
return 0;
}
return Math.round(
(100 / session.NowPlayingItem?.RunTimeTicks) *
(session.NowPlayingItem?.RunTimeTicks - remainingTicks),
);
};
useEffect(() => {
const currentTime = session.PlayState?.PositionTicks;
const duration = session.NowPlayingItem?.RunTimeTicks;
if (
duration !== null &&
duration !== undefined &&
currentTime !== null &&
currentTime !== undefined
) {
const remainingTimeTicks = duration - currentTime;
setRemainingTicks(remainingTimeTicks);
}
}, [session]);
const { data: ipInfo } = useQuery({
queryKey: ["ipinfo", session.RemoteEndPoint],
cacheTime: Number.POSITIVE_INFINITY,
queryFn: async () => {
const resp = await api.axiosInstance.get(
`https://freeipapi.com/api/json/${session.RemoteEndPoint}`,
);
return resp.data;
},
});
// Handle session controls
const [isControlLoading, setIsControlLoading] = useState<
Record<string, boolean>
>({});
const handleSystemCommand = async (command: GeneralCommandType) => {
if (!api || !session.Id) return false;
setIsControlLoading({ ...isControlLoading, [command]: true });
try {
getSessionApi(api).sendSystemCommand({
sessionId: session.Id,
command,
});
return true;
} catch (error) {
console.error(`Error sending ${command} command:`, error);
return false;
} finally {
setIsControlLoading({ ...isControlLoading, [command]: false });
}
};
const handlePlaystateCommand = async (command: PlaystateCommand) => {
if (!api || !session.Id) return false;
setIsControlLoading({ ...isControlLoading, [command]: true });
try {
getSessionApi(api).sendPlaystateCommand({
sessionId: session.Id,
command,
});
return true;
} catch (error) {
console.error(`Error sending playstate ${command} command:`, error);
return false;
} finally {
setIsControlLoading({ ...isControlLoading, [command]: false });
}
};
const handlePlayPause = async () => {
console.log("handlePlayPause");
await handlePlaystateCommand(PlaystateCommand.PlayPause);
};
const handleStop = async () => {
await handlePlaystateCommand(PlaystateCommand.Stop);
};
const handlePrevious = async () => {
await handlePlaystateCommand(PlaystateCommand.PreviousTrack);
};
const handleNext = async () => {
await handlePlaystateCommand(PlaystateCommand.NextTrack);
};
const handleToggleMute = async () => {
await handleSystemCommand(GeneralCommandType.ToggleMute);
};
const handleVolumeUp = async () => {
await handleSystemCommand(GeneralCommandType.VolumeUp);
};
const handleVolumeDown = async () => {
await handleSystemCommand(GeneralCommandType.VolumeDown);
};
useInterval(tick, 1000);
return (
<View className='flex flex-col shadow-md bg-neutral-900 rounded-2xl mb-4'>
<View className='flex flex-row p-4'>
<View className='w-20 pr-4'>
<Poster
id={session.NowPlayingItem?.Id}
url={getPrimaryImageUrl({ api, item: session.NowPlayingItem })}
/>
</View>
<View className='w-full flex-1'>
<View className='flex flex-row justify-between'>
<View className='flex-1 pr-4'>
{session.NowPlayingItem?.Type === "Episode" ? (
<>
<Text className='font-bold'>
{session.NowPlayingItem?.Name}
</Text>
<Text numberOfLines={1} className='text-xs opacity-50'>
{`S${session.NowPlayingItem.ParentIndexNumber?.toString()}:E${session.NowPlayingItem.IndexNumber?.toString()}`}
{" - "}
{session.NowPlayingItem.SeriesName}
</Text>
</>
) : (
<>
<Text className='font-bold'>
{session.NowPlayingItem?.Name}
</Text>
<Text className='text-xs opacity-50'>
{session.NowPlayingItem?.ProductionYear}
</Text>
<Text className='text-xs opacity-50'>
{session.NowPlayingItem?.SeriesName}
</Text>
</>
)}
</View>
<Text className='text-xs opacity-50 align-right text-right'>
{session.UserName}
{"\n"}
{session.Client}
{"\n"}
{session.DeviceName}
{"\n"}
{ipInfo?.cityName} {ipInfo?.countryCode}
</Text>
</View>
<View className='flex-1' />
<View className='flex flex-col align-bottom'>
<View className='flex flex-row justify-between align-bottom mb-1'>
<Text className='-ml-0.5 text-xs opacity-50 align-left text-left'>
{!session.PlayState?.IsPaused ? (
<Ionicons name='play' size={14} color='white' />
) : (
<Ionicons name='pause' size={14} color='white' />
)}
</Text>
<Text className='text-xs opacity-50 align-right text-right'>
{formatTimeString(remainingTicks, "tick")} left
</Text>
</View>
<View className='align-bottom bg-gray-800 h-1'>
<View
className={"bg-purple-600 h-full"}
style={{
width: `${getProgressPercentage()}%`,
}}
/>
</View>
{/* Session controls */}
<View className='flex flex-row mt-2 space-x-4 justify-center'>
<TouchableOpacity
onPress={handlePrevious}
disabled={isControlLoading[PlaystateCommand.PreviousTrack]}
style={{
opacity: isControlLoading[PlaystateCommand.PreviousTrack]
? 0.5
: 1,
}}
>
<MaterialCommunityIcons
name='skip-previous'
size={24}
color='white'
/>
</TouchableOpacity>
<TouchableOpacity
onPress={handlePlayPause}
disabled={isControlLoading[PlaystateCommand.PlayPause]}
style={{
opacity: isControlLoading[PlaystateCommand.PlayPause]
? 0.5
: 1,
}}
>
{session.PlayState?.IsPaused ? (
<Ionicons name='play' size={24} color='white' />
) : (
<Ionicons name='pause' size={24} color='white' />
)}
</TouchableOpacity>
<TouchableOpacity
onPress={handleStop}
disabled={isControlLoading[PlaystateCommand.Stop]}
style={{
opacity: isControlLoading[PlaystateCommand.Stop] ? 0.5 : 1,
}}
>
<Ionicons name='stop' size={24} color='white' />
</TouchableOpacity>
<TouchableOpacity
onPress={handleNext}
disabled={isControlLoading[PlaystateCommand.NextTrack]}
style={{
opacity: isControlLoading[PlaystateCommand.NextTrack]
? 0.5
: 1,
}}
>
<MaterialCommunityIcons
name='skip-next'
size={24}
color='white'
/>
</TouchableOpacity>
<TouchableOpacity
onPress={handleVolumeDown}
disabled={isControlLoading[GeneralCommandType.VolumeDown]}
style={{
opacity: isControlLoading[GeneralCommandType.VolumeDown]
? 0.5
: 1,
}}
>
<Ionicons name='volume-low' size={24} color='white' />
</TouchableOpacity>
<TouchableOpacity
onPress={handleToggleMute}
disabled={isControlLoading[GeneralCommandType.ToggleMute]}
style={{
opacity: isControlLoading[GeneralCommandType.ToggleMute]
? 0.5
: 1,
}}
>
<Ionicons
name='volume-mute'
size={24}
color={session.PlayState?.IsMuted ? "red" : "white"}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={handleVolumeUp}
disabled={isControlLoading[GeneralCommandType.VolumeUp]}
style={{
opacity: isControlLoading[GeneralCommandType.VolumeUp]
? 0.5
: 1,
}}
>
<Ionicons name='volume-high' size={24} color='white' />
</TouchableOpacity>
</View>
</View>
</View>
</View>
<TranscodingView session={session} />
</View>
);
};
interface TranscodingBadgesProps {
properties: StreamProps;
}
const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
const iconMap = {
bitrate: <Ionicons name='speedometer-outline' size={12} color='white' />,
codec: <Ionicons name='layers-outline' size={12} color='white' />,
videoRange: (
<Ionicons name='color-palette-outline' size={12} color='white' />
),
resolution: <Ionicons name='film-outline' size={12} color='white' />,
language: <Ionicons name='language-outline' size={12} color='white' />,
audioChannels: <Ionicons name='mic-outline' size={12} color='white' />,
hwType: <Ionicons name='hardware-chip-outline' size={12} color='white' />,
} as const;
const icon = (val: string) => {
return (
iconMap[val as keyof typeof iconMap] ?? (
<Ionicons name='layers-outline' size={12} color='white' />
)
);
};
const formatVal = (key: string, val: any) => {
switch (key) {
case "bitrate":
return formatBitrate(val);
case "hwType":
return val === HardwareAccelerationType.None ? "sw" : "hw";
default:
return val;
}
};
return Object.entries(properties)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key]) => (
<Badge
key={key}
variant='gray'
className='m-0 p-0 pt-0.5 mr-1'
text={formatVal(key, properties[key as keyof StreamProps])}
iconLeft={icon(key)}
/>
));
};
interface StreamProps {
hwType?: HardwareAccelerationType | null | undefined;
resolution?: string | null | undefined;
language?: string | null | undefined;
codec?: string | null | undefined;
bitrate?: number | null | undefined;
videoRange?: string | null | undefined;
audioChannels?: string | null | undefined;
}
interface TranscodingStreamViewProps {
title: string | undefined;
value?: string;
isTranscoding: boolean;
transcodeValue?: string | undefined | null;
properties: StreamProps;
transcodeProperties?: StreamProps;
}
const TranscodingStreamView = ({
title,
isTranscoding,
properties,
transcodeProperties,
value,
transcodeValue,
}: TranscodingStreamViewProps) => {
return (
<View className='flex flex-col pt-2 first:pt-0'>
<View className='flex flex-row'>
<Text className='text-xs opacity-50 w-20 font-bold text-right pr-4'>
{title}
</Text>
<Text className='flex-1'>
<TranscodingBadges properties={properties} />
</Text>
</View>
{isTranscoding && transcodeProperties ? (
<>
<View className='flex flex-row'>
<Text className='-mt-0 text-xs opacity-50 w-20 font-bold text-right pr-4'>
<MaterialCommunityIcons
name='arrow-right-bottom'
size={14}
color='white'
/>
</Text>
<Text className='flex-1 text-sm mt-1'>
<TranscodingBadges properties={transcodeProperties} />
</Text>
</View>
</>
) : null}
</View>
);
};
const TranscodingView = ({ session }: SessionCardProps) => {
const videoStream = useMemo(() => {
return session.NowPlayingItem?.MediaStreams?.filter(
(s) => s.Type === "Video",
)[0];
}, [session]);
const audioStream = useMemo(() => {
const index = session.PlayState?.AudioStreamIndex;
return index !== null && index !== undefined
? session.NowPlayingItem?.MediaStreams?.[index]
: undefined;
}, [session.PlayState?.AudioStreamIndex]);
const subtitleStream = useMemo(() => {
const index = session.PlayState?.SubtitleStreamIndex;
return index !== null && index !== undefined
? session.NowPlayingItem?.MediaStreams?.[index]
: undefined;
}, [session.PlayState?.SubtitleStreamIndex]);
const isTranscoding = useMemo(() => {
return (
session.PlayState?.PlayMethod === "Transcode" && session.TranscodingInfo
);
}, [session.PlayState?.PlayMethod, session.TranscodingInfo]);
const videoStreamTitle = () => {
return videoStream?.DisplayTitle?.split(" ")[0];
};
return (
<View className='flex flex-col bg-neutral-800 rounded-b-2xl p-4 pt-2'>
<TranscodingStreamView
title='Video'
properties={{
resolution: videoStreamTitle(),
bitrate: videoStream?.BitRate,
codec: videoStream?.Codec,
}}
transcodeProperties={{
hwType: session.TranscodingInfo?.HardwareAccelerationType,
bitrate: session.TranscodingInfo?.Bitrate,
codec: session.TranscodingInfo?.VideoCodec,
}}
isTranscoding={
!!(isTranscoding && !session.TranscodingInfo?.IsVideoDirect)
}
/>
<TranscodingStreamView
title='Audio'
properties={{
language: audioStream?.Language,
bitrate: audioStream?.BitRate,
codec: audioStream?.Codec,
audioChannels: audioStream?.ChannelLayout,
}}
transcodeProperties={{
codec: session.TranscodingInfo?.AudioCodec,
audioChannels: session.TranscodingInfo?.AudioChannels?.toString(),
}}
isTranscoding={
!!(isTranscoding && !session.TranscodingInfo?.IsVideoDirect)
}
/>
{subtitleStream && (
<TranscodingStreamView
title='Subtitle'
isTranscoding={false}
properties={{
language: subtitleStream?.Language,
codec: subtitleStream?.Codec,
}}
transcodeValue={null}
/>
)}
</View>
);
};

View File

@@ -0,0 +1,120 @@
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
import { AudioToggles } from "@/components/settings/AudioToggles";
import { ChromecastSettings } from "@/components/settings/ChromecastSettings";
import DownloadSettings from "@/components/settings/DownloadSettings";
import { MediaProvider } from "@/components/settings/MediaContext";
import { MediaToggles } from "@/components/settings/MediaToggles";
import { OtherSettings } from "@/components/settings/OtherSettings";
import { PluginSettings } from "@/components/settings/PluginSettings";
import { QuickConnect } from "@/components/settings/QuickConnect";
import { StorageSettings } from "@/components/settings/StorageSettings";
import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
import { UserInfo } from "@/components/settings/UserInfo";
import { useHaptic } from "@/hooks/useHaptic";
import { useJellyfin } from "@/providers/JellyfinProvider";
import { userAtom } from "@/providers/JellyfinProvider";
import { clearLogs } from "@/utils/log";
import { storage } from "@/utils/mmkv";
import { useNavigation, useRouter } from "expo-router";
import { t } from "i18next";
import { useAtom } from "jotai";
import React, { useEffect } from "react";
import { ScrollView, Switch, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function settings() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [user] = useAtom(userAtom);
const { logout } = useJellyfin();
const successHapticFeedback = useHaptic("success");
const onClearLogsClicked = async () => {
clearLogs();
successHapticFeedback();
};
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity
onPress={() => {
logout();
}}
>
<Text className='text-red-600'>
{t("home.settings.log_out_button")}
</Text>
</TouchableOpacity>
),
});
}, []);
return (
<ScrollView
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<View className='p-4 flex flex-col gap-y-4'>
<UserInfo />
<QuickConnect className='mb-4' />
<MediaProvider>
<MediaToggles className='mb-4' />
<AudioToggles className='mb-4' />
<SubtitleToggles className='mb-4' />
</MediaProvider>
<OtherSettings />
<DownloadSettings />
<PluginSettings />
<AppLanguageSelector />
<ChromecastSettings />
<ListGroup title={"Intro"}>
<ListItem
onPress={() => {
router.push("/intro/page");
}}
title={t("home.settings.intro.show_intro")}
/>
<ListItem
textColor='red'
onPress={() => {
storage.set("hasShownIntro", false);
}}
title={t("home.settings.intro.reset_intro")}
/>
</ListGroup>
<View className='mb-4'>
<ListGroup title={t("home.settings.logs.logs_title")}>
<ListItem
onPress={() => router.push("/settings/logs/page")}
showArrow
title={t("home.settings.logs.logs_title")}
/>
<ListItem
textColor='red'
onPress={onClearLogsClicked}
title={t("home.settings.logs.delete_all_logs")}
/>
</ListGroup>
</View>
<StorageSettings />
</View>
</ScrollView>
);
}

View File

@@ -0,0 +1,67 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
import { Switch, View } from "react-native";
export default function page() {
const [settings, updateSettings, pluginSettings] = useSettings();
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
const { t } = useTranslation();
const { data, isLoading } = useQuery({
queryKey: ["user-views", user?.Id],
queryFn: async () => {
const response = await getUserViewsApi(api!).getUserViews({
userId: user?.Id,
});
return response.data.Items || null;
},
});
if (!settings) return null;
if (isLoading)
return (
<View className='mt-4'>
<Loader />
</View>
);
return (
<DisabledSetting
disabled={pluginSettings?.hiddenLibraries?.locked === true}
className='px-4'
>
<ListGroup>
{data?.map((view) => (
<ListItem key={view.Id} title={view.Name} onPress={() => {}}>
<Switch
value={settings.hiddenLibraries?.includes(view.Id!) || false}
onValueChange={(value) => {
updateSettings({
hiddenLibraries: value
? [...(settings.hiddenLibraries || []), view.Id!]
: settings.hiddenLibraries?.filter((id) => id !== view.Id),
});
}}
/>
</ListItem>
))}
</ListGroup>
<Text className='px-4 text-xs text-neutral-500 mt-1'>
{t("home.settings.other.select_liraries_you_want_to_hide")}
</Text>
</DisabledSetting>
);
}

View File

@@ -0,0 +1,16 @@
import DisabledSetting from "@/components/settings/DisabledSetting";
import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
import { useSettings } from "@/utils/atoms/settings";
export default function page() {
const [settings, updateSettings, pluginSettings] = useSettings();
return (
<DisabledSetting
disabled={pluginSettings?.jellyseerrServerUrl?.locked === true}
className='p-4'
>
<JellyseerrSettings />
</DisabledSetting>
);
}

View File

@@ -0,0 +1,157 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { FilterButton } from "@/components/filters/FilterButton";
import { LogLevel, useLog, writeErrorLog } from "@/utils/log";
import * as FileSystem from "expo-file-system";
import { useNavigation } from "expo-router";
import * as Sharing from "expo-sharing";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, TouchableOpacity, View } from "react-native";
import Collapsible from "react-native-collapsible";
export default function page() {
const navigation = useNavigation();
const { logs } = useLog();
const { t } = useTranslation();
const defaultLevels: LogLevel[] = ["INFO", "ERROR", "DEBUG", "WARN"];
const codeBlockStyle = {
backgroundColor: "#000",
padding: 10,
fontFamily: "monospace",
maxHeight: 300,
};
const [loading, setLoading] = useState<boolean>(false);
const [state, setState] = useState<Record<string, boolean>>({});
const [order, setOrder] = useState<"asc" | "desc">("desc");
const [levels, setLevels] = useState<LogLevel[]>(defaultLevels);
const filteredLogs = useMemo(
() =>
logs
?.filter((log) => levels.includes(log.level))
?.[
// Already in asc order as they are recorded. just reverse for desc
order === "desc" ? "reverse" : "concat"
]?.(),
[logs, order, levels],
);
// Sharing it as txt while its formatted allows us to share it with many more applications
const share = useCallback(async () => {
const uri = `${FileSystem.documentDirectory}logs.txt`;
setLoading(true);
FileSystem.writeAsStringAsync(uri, JSON.stringify(filteredLogs))
.then(() => {
setLoading(false);
Sharing.shareAsync(uri, { mimeType: "txt", UTI: "txt" });
})
.catch((e) =>
writeErrorLog("Something went wrong attempting to export", e),
)
.finally(() => setLoading(false));
}, [filteredLogs]);
useEffect(() => {
navigation.setOptions({
headerRight: () =>
loading ? (
<Loader />
) : (
<TouchableOpacity onPress={share}>
<Text>{t("home.settings.logs.export_logs")}</Text>
</TouchableOpacity>
),
});
}, [share, loading]);
return (
<>
<View className='flex flex-row justify-end py-2 px-4 space-x-2'>
<FilterButton
id='order'
queryKey='log'
queryFn={async () => ["asc", "desc"]}
set={(values) => setOrder(values[0])}
values={[order]}
title={t("library.filters.sort_order")}
renderItemLabel={(order) => t(`library.filters.${order}`)}
showSearch={false}
/>
<FilterButton
id='levels'
queryKey='log'
queryFn={async () => defaultLevels}
set={setLevels}
values={levels}
title={t("home.settings.logs.level")}
renderItemLabel={(level) => level}
showSearch={false}
multiple={true}
/>
</View>
<ScrollView className='pb-4 px-4'>
<View className='flex flex-col space-y-2'>
{filteredLogs?.map((log, index) => (
<View className='bg-neutral-900 rounded-xl p-3' key={index}>
<TouchableOpacity
disabled={!log.data}
onPress={() =>
setState((v) => ({
...v,
[log.timestamp]: !v[log.timestamp],
}))
}
>
<View className='flex flex-row justify-between'>
<Text
className={`mb-1
${log.level === "INFO" && "text-blue-500"}
${log.level === "ERROR" && "text-red-500"}
${log.level === "DEBUG" && "text-purple-500"}
`}
>
{log.level}
</Text>
<Text className='text-xs'>
{new Date(log.timestamp).toLocaleString()}
</Text>
</View>
<Text uiTextView selectable className='text-xs'>
{log.message}
</Text>
</TouchableOpacity>
{log.data && (
<>
{!state[log.timestamp] && (
<Text className='text-xs mt-0.5'>
{t("home.settings.logs.click_for_more_info")}
</Text>
)}
<Collapsible collapsed={!state[log.timestamp]}>
<View className='mt-2 flex flex-col space-y-2'>
<ScrollView className='rounded-xl' style={codeBlockStyle}>
<Text>{JSON.stringify(log.data, null, 2)}</Text>
</ScrollView>
</View>
</Collapsible>
</>
)}
</View>
))}
{filteredLogs?.length === 0 && (
<Text className='opacity-50'>
{t("home.settings.logs.no_logs_available")}
</Text>
)}
</View>
</ScrollView>
</>
);
}

View File

@@ -0,0 +1,123 @@
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { useSettings } from "@/utils/atoms/settings";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useTranslation } from "react-i18next";
import DisabledSetting from "@/components/settings/DisabledSetting";
import React, { useEffect, useMemo, useState } from "react";
import {
Linking,
Switch,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { toast } from "sonner-native";
export default function page() {
const navigation = useNavigation();
const { t } = useTranslation();
const [settings, updateSettings, pluginSettings] = useSettings();
const queryClient = useQueryClient();
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
const onSave = (val: string) => {
updateSettings({
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
});
toast.success(t("home.settings.plugins.marlin_search.toasts.saved"));
};
const handleOpenLink = () => {
Linking.openURL("https://github.com/fredrikburmester/marlin-search");
};
const disabled = useMemo(() => {
return (
pluginSettings?.searchEngine?.locked === true &&
pluginSettings?.marlinServerUrl?.locked === true
);
}, [pluginSettings]);
useEffect(() => {
if (!pluginSettings?.marlinServerUrl?.locked) {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => onSave(value)}>
<Text className='text-blue-500'>
{t("home.settings.plugins.marlin_search.save_button")}
</Text>
</TouchableOpacity>
),
});
}
}, [navigation, value]);
if (!settings) return null;
return (
<DisabledSetting disabled={disabled} className='px-4'>
<ListGroup>
<DisabledSetting
disabled={pluginSettings?.searchEngine?.locked === true}
showText={!pluginSettings?.marlinServerUrl?.locked}
>
<ListItem
title={t(
"home.settings.plugins.marlin_search.enable_marlin_search",
)}
onPress={() => {
updateSettings({ searchEngine: "Jellyfin" });
queryClient.invalidateQueries({ queryKey: ["search"] });
}}
>
<Switch
value={settings.searchEngine === "Marlin"}
onValueChange={(value) => {
updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" });
queryClient.invalidateQueries({ queryKey: ["search"] });
}}
/>
</ListItem>
</DisabledSetting>
</ListGroup>
<DisabledSetting
disabled={pluginSettings?.marlinServerUrl?.locked === true}
showText={!pluginSettings?.searchEngine?.locked}
className='mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'
>
<View className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}>
<Text className='mr-4'>
{t("home.settings.plugins.marlin_search.url")}
</Text>
<TextInput
editable={settings.searchEngine === "Marlin"}
className='text-white'
placeholder={t(
"home.settings.plugins.marlin_search.server_url_placeholder",
)}
value={value}
keyboardType='url'
returnKeyType='done'
autoCapitalize='none'
textContentType='URL'
onChangeText={(text) => setValue(text)}
/>
</View>
</DisabledSetting>
<Text className='px-4 text-xs text-neutral-500 mt-1'>
{t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "}
<Text className='text-blue-500' onPress={handleOpenLink}>
{t("home.settings.plugins.marlin_search.read_more_about_marlin")}
</Text>
</Text>
</DisabledSetting>
);
}

View File

@@ -0,0 +1,93 @@
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device";
import { getStatistics } from "@/utils/optimize-server";
import { useMutation } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import { toast } from "sonner-native";
export default function page() {
const navigation = useNavigation();
const { t } = useTranslation();
const [api] = useAtom(apiAtom);
const [settings, updateSettings, pluginSettings] = useSettings();
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
useState<string>(settings?.optimizedVersionsServerUrl || "");
const saveMutation = useMutation({
mutationFn: async (newVal: string) => {
if (newVal.length === 0 || !newVal.startsWith("http")) {
toast.error(t("home.settings.toasts.invalid_url"));
return;
}
const updatedUrl = newVal.endsWith("/") ? newVal : `${newVal}/`;
updateSettings({
optimizedVersionsServerUrl: updatedUrl,
});
return await getStatistics({
url: updatedUrl,
authHeader: api?.accessToken,
deviceId: getOrSetDeviceId(),
});
},
onSuccess: (data) => {
if (data) {
toast.success(t("home.settings.toasts.connected"));
} else {
toast.error(t("home.settings.toasts.could_not_connect"));
}
},
onError: () => {
toast.error(t("home.settings.toasts.could_not_connect"));
},
});
const onSave = (newVal: string) => {
saveMutation.mutate(newVal);
};
useEffect(() => {
if (!pluginSettings?.optimizedVersionsServerUrl?.locked) {
navigation.setOptions({
title: t("home.settings.downloads.optimized_server"),
headerRight: () =>
saveMutation.isPending ? (
<ActivityIndicator size={"small"} color={"white"} />
) : (
<TouchableOpacity
onPress={() => onSave(optimizedVersionsServerUrl)}
>
<Text className='text-blue-500'>
{t("home.settings.downloads.save_button")}
</Text>
</TouchableOpacity>
),
});
}
}, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]);
return (
<DisabledSetting
disabled={pluginSettings?.optimizedVersionsServerUrl?.locked === true}
className='p-4'
>
<OptimizedServerForm
value={optimizedVersionsServerUrl}
onChangeValue={setOptimizedVersionsServerUrl}
/>
</DisabledSetting>
);
}

View File

@@ -0,0 +1,142 @@
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import type { BaseItemDtoQueryResult } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
const page: React.FC = () => {
const local = useLocalSearchParams();
const { actorId } = local as { actorId: string };
const { t } = useTranslation();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: item, isLoading: l1 } = useQuery({
queryKey: ["item", actorId],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: actorId,
}),
enabled: !!actorId && !!api,
staleTime: 60,
});
const fetchItems = useCallback(
async ({
pageParam,
}: {
pageParam: number;
}): Promise<BaseItemDtoQueryResult | null> => {
if (!api || !user?.Id) return null;
const response = await getItemsApi(api).getItems({
userId: user.Id,
personIds: [actorId],
startIndex: pageParam,
limit: 16,
sortOrder: ["Descending", "Descending", "Ascending"],
includeItemTypes: ["Movie", "Series"],
recursive: true,
fields: [
"ParentId",
"PrimaryImageAspectRatio",
"ParentId",
"PrimaryImageAspectRatio",
],
sortBy: ["PremiereDate", "ProductionYear", "SortName"],
collapseBoxSetItems: false,
});
return response.data;
},
[api, user?.Id, actorId],
);
const backdropUrl = useMemo(
() =>
getBackdropUrl({
api,
item,
quality: 90,
width: 1000,
}),
[item],
);
if (l1)
return (
<View className='justify-center items-center h-full'>
<Loader />
</View>
);
if (!item?.Id || !backdropUrl) return null;
return (
<ParallaxScrollView
headerImage={
<Image
source={{
uri: backdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
}
>
<View className='flex flex-col space-y-4 my-4'>
<View className='px-4 mb-4'>
<MoviesTitleHeader item={item} className='mb-4' />
<OverviewText text={item.Overview} />
</View>
<Text className='px-4 text-2xl font-bold mb-2 text-neutral-100'>
{t("item_card.appeared_in")}
</Text>
<InfiniteHorizontalScroll
height={247}
renderItem={(i, idx) => (
<TouchableItemRouter
key={idx}
item={i}
className={`flex flex-col
${"w-28"}
`}
>
<View>
<MoviePoster item={i} />
<ItemCardText item={i} />
</View>
</TouchableItemRouter>
)}
queryFn={fetchItems}
queryKey={["actor", "movies", actorId]}
/>
<View className='h-12' />
</View>
</ParallaxScrollView>
);
};
export default page;

View File

@@ -0,0 +1,420 @@
import { ItemCardText } from "@/components/ItemCardText";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemPoster } from "@/components/posters/ItemPoster";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
SortByOption,
SortOrderOption,
genreFilterAtom,
sortByAtom,
sortOptions,
sortOrderAtom,
sortOrderOptions,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import type {
BaseItemDto,
BaseItemDtoQueryResult,
ItemSortBy,
} from "@jellyfin/sdk/lib/generated-client/models";
import {
getFilterApi,
getItemsApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, View } from "react-native";
const page: React.FC = () => {
const searchParams = useLocalSearchParams();
const { collectionId } = searchParams as { collectionId: string };
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const navigation = useNavigation();
const [orientation, setOrientation] = useState(
ScreenOrientation.Orientation.PORTRAIT_UP,
);
const { t } = useTranslation();
const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom);
const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom);
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
const [sortBy, setSortBy] = useAtom(sortByAtom);
const [sortOrder, setSortOrder] = useAtom(sortOrderAtom);
const { data: collection } = useQuery({
queryKey: ["collection", collectionId],
queryFn: async () => {
if (!api) return null;
const response = await getUserLibraryApi(api).getItem({
itemId: collectionId,
userId: user?.Id,
});
const data = response.data;
return data;
},
enabled: !!api && !!user?.Id && !!collectionId,
staleTime: 60 * 1000,
});
useEffect(() => {
navigation.setOptions({ title: collection?.Name || "" });
setSortOrder([SortOrderOption.Ascending]);
if (!collection) return;
// Convert the DisplayOrder to SortByOption
const displayOrder = collection.DisplayOrder as ItemSortBy;
const sortByOption = displayOrder
? SortByOption[displayOrder as keyof typeof SortByOption] ||
SortByOption.PremiereDate
: SortByOption.PremiereDate;
setSortBy([sortByOption]);
}, [navigation, collection]);
const fetchItems = useCallback(
async ({
pageParam,
}: {
pageParam: number;
}): Promise<BaseItemDtoQueryResult | null> => {
if (!api || !collection) return null;
const response = await getItemsApi(api).getItems({
userId: user?.Id,
parentId: collectionId,
limit: 18,
startIndex: pageParam,
// Set one ordering at a time. As collections do not work with correctly with multiple.
sortBy: [sortBy[0]],
sortOrder: [sortOrder[0]],
fields: [
"ItemCounts",
"PrimaryImageAspectRatio",
"CanDelete",
"MediaSourceCount",
],
// true is needed for merged versions
recursive: true,
genres: selectedGenres,
tags: selectedTags,
years: selectedYears.map((year) => Number.parseInt(year)),
includeItemTypes: ["Movie", "Series"],
});
return response.data || null;
},
[
api,
user?.Id,
collection,
selectedGenres,
selectedYears,
selectedTags,
sortBy,
sortOrder,
],
);
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: [
"collection-items",
collection,
selectedGenres,
selectedYears,
selectedTags,
sortBy,
sortOrder,
],
queryFn: fetchItems,
getNextPageParam: (lastPage, pages) => {
if (
!lastPage?.Items ||
!lastPage?.TotalRecordCount ||
lastPage?.TotalRecordCount === 0
)
return undefined;
const totalItems = lastPage.TotalRecordCount;
const accumulatedItems = pages.reduce(
(acc, curr) => acc + (curr?.Items?.length || 0),
0,
);
if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length;
}
return undefined;
},
initialPageParam: 0,
enabled: !!api && !!user?.Id && !!collection,
});
const flatData = useMemo(() => {
return (
(data?.pages.flatMap((p) => p?.Items).filter(Boolean) as BaseItemDto[]) ||
[]
);
}, [data]);
const renderItem = useCallback(
({ item, index }: { item: BaseItemDto; index: number }) => (
<TouchableItemRouter
key={item.Id}
style={{
width: "100%",
marginBottom:
orientation === ScreenOrientation.Orientation.PORTRAIT_UP ? 4 : 16,
}}
item={item}
>
<View
style={{
alignSelf:
index % 3 === 0
? "flex-end"
: (index + 1) % 3 === 0
? "flex-start"
: "center",
width: "89%",
}}
>
<ItemPoster item={item} />
{/* <MoviePoster item={item} /> */}
<ItemCardText item={item} />
</View>
</TouchableItemRouter>
),
[orientation],
);
const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []);
const ListHeaderComponent = useCallback(
() => (
<View className=''>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
display: "flex",
paddingHorizontal: 15,
paddingVertical: 16,
flexDirection: "row",
}}
extraData={[
selectedGenres,
selectedYears,
selectedTags,
sortBy,
sortOrder,
]}
data={[
{
key: "reset",
component: <ResetFiltersButton />,
},
{
key: "genre",
component: (
<FilterButton
className='mr-1'
id={collectionId}
queryKey='genreFilter'
queryFn={async () => {
if (!api) return null;
const response = await getFilterApi(
api,
).getQueryFiltersLegacy({
userId: user?.Id,
parentId: collectionId,
});
return response.data.Genres || [];
}}
set={setSelectedGenres}
values={selectedGenres}
title={t("library.filters.genres")}
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
{
key: "year",
component: (
<FilterButton
className='mr-1'
id={collectionId}
queryKey='yearFilter'
queryFn={async () => {
if (!api) return null;
const response = await getFilterApi(
api,
).getQueryFiltersLegacy({
userId: user?.Id,
parentId: collectionId,
});
return response.data.Years || [];
}}
set={setSelectedYears}
values={selectedYears}
title={t("library.filters.years")}
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) => item.includes(search)}
/>
),
},
{
key: "tags",
component: (
<FilterButton
className='mr-1'
id={collectionId}
queryKey='tagsFilter'
queryFn={async () => {
if (!api) return null;
const response = await getFilterApi(
api,
).getQueryFiltersLegacy({
userId: user?.Id,
parentId: collectionId,
});
return response.data.Tags || [];
}}
set={setSelectedTags}
values={selectedTags}
title={t("library.filters.tags")}
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
{
key: "sortBy",
component: (
<FilterButton
className='mr-1'
id={collectionId}
queryKey='sortBy'
queryFn={async () => sortOptions.map((s) => s.key)}
set={setSortBy}
values={sortBy}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
sortOptions.find((i) => i.key === item)?.value || ""
}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
{
key: "sortOrder",
component: (
<FilterButton
className='mr-1'
id={collectionId}
queryKey='sortOrder'
queryFn={async () => sortOrderOptions.map((s) => s.key)}
set={setSortOrder}
values={sortOrder}
title={t("library.filters.sort_order")}
renderItemLabel={(item) =>
sortOrderOptions.find((i) => i.key === item)?.value || ""
}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
]}
renderItem={({ item }) => item.component}
keyExtractor={(item) => item.key}
/>
</View>
),
[
collectionId,
api,
user?.Id,
selectedGenres,
setSelectedGenres,
selectedYears,
setSelectedYears,
selectedTags,
setSelectedTags,
sortBy,
setSortBy,
sortOrder,
setSortOrder,
isFetching,
],
);
if (!collection) return null;
return (
<FlashList
ListEmptyComponent={
<View className='flex flex-col items-center justify-center h-full'>
<Text className='font-bold text-xl text-neutral-500'>
{t("search.no_results")}
</Text>
</View>
}
extraData={[
selectedGenres,
selectedYears,
selectedTags,
sortBy,
sortOrder,
]}
contentInsetAdjustmentBehavior='automatic'
data={flatData}
renderItem={renderItem}
keyExtractor={keyExtractor}
estimatedItemSize={255}
numColumns={
orientation === ScreenOrientation.Orientation.PORTRAIT_UP ? 3 : 5
}
onEndReached={() => {
if (hasNextPage) {
fetchNextPage();
}
}}
onEndReachedThreshold={0.5}
ListHeaderComponent={ListHeaderComponent}
contentContainerStyle={{ paddingBottom: 24 }}
ItemSeparatorComponent={() => (
<View
style={{
width: 10,
height: 10,
}}
/>
)}
/>
);
};
export default page;

View File

@@ -0,0 +1,115 @@
import { ItemContent } from "@/components/ItemContent";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import type React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
const Page: React.FC = () => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { id } = useLocalSearchParams() as { id: string };
const { t } = useTranslation();
const { data: item, isError } = useQuery({
queryKey: ["item", id],
queryFn: async () => {
if (!api || !user || !id) return;
const res = await getUserLibraryApi(api).getItem({
itemId: id,
userId: user?.Id,
});
return res.data;
},
staleTime: 0,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
});
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: opacity.value,
};
});
const fadeOut = (callback: any) => {
setTimeout(() => {
opacity.value = withTiming(0, { duration: 500 }, (finished) => {
if (finished) {
runOnJS(callback)();
}
});
}, 100);
};
const fadeIn = (callback: any) => {
setTimeout(() => {
opacity.value = withTiming(1, { duration: 500 }, (finished) => {
if (finished) {
runOnJS(callback)();
}
});
}, 100);
};
useEffect(() => {
if (item) {
fadeOut(() => {});
} else {
fadeIn(() => {});
}
}, [item]);
if (isError)
return (
<View className='flex flex-col items-center justify-center h-screen w-screen'>
<Text>{t("item_card.could_not_load_item")}</Text>
</View>
);
return (
<View className='flex flex-1 relative'>
<Animated.View
pointerEvents={"none"}
style={[animatedStyle]}
className='absolute top-0 left-0 flex flex-col items-start h-screen w-screen px-4 z-50 bg-black'
>
<View
style={{
height: item?.Type === "Episode" ? 300 : 450,
}}
className='bg-transparent rounded-lg mb-4 w-full'
/>
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
<View className='flex flex-row space-x-1 mb-8'>
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
</View>
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2' />
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
</Animated.View>
{item && <ItemContent item={item} />}
</View>
);
};
export default Page;

View File

@@ -0,0 +1,107 @@
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import {
type MovieResult,
Results,
type TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { uniqBy } from "lodash";
import React, { useMemo } from "react";
export default function page() {
const local = useLocalSearchParams();
const { jellyseerrApi } = useJellyseerr();
const { companyId, name, image, type } = local as unknown as {
companyId: string;
name: string;
image: string;
type: DiscoverSliderType;
};
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "company", type, companyId],
queryFn: async ({ pageParam }) => {
const params: any = {
page: Number(pageParam),
};
return jellyseerrApi?.discover(
`${
type === DiscoverSliderType.NETWORKS
? Endpoints.DISCOVER_TV_NETWORK
: Endpoints.DISCOVER_MOVIES_STUDIO
}/${companyId}`,
params,
);
},
enabled: !!jellyseerrApi && !!companyId,
initialPageParam: 1,
getNextPageParam: (lastPage, pages) =>
(lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
1,
staleTime: 0,
});
const flatData = useMemo(
() =>
uniqBy(
data?.pages
?.filter((p) => p?.results.length)
.flatMap((p) => p?.results ?? []),
"id",
) ?? [],
[data],
);
const backdrops = useMemo(
() =>
jellyseerrApi
? flatData.map((r) =>
jellyseerrApi.imageProxy(
(r as TvResult | MovieResult).backdropPath,
"w1920_and_h800_multi_faces",
),
)
: [],
[jellyseerrApi, flatData],
);
return (
<ParallaxSlideShow
data={flatData}
images={backdrops}
listHeader=''
keyExtractor={(item) => item.id.toString()}
onEndReached={() => {
if (hasNextPage) {
fetchNextPage();
}
}}
logo={
<Image
id={companyId}
key={companyId}
className='bottom-1 w-1/2'
source={{
uri: jellyseerrApi?.imageProxy(image, COMPANY_LOGO_IMAGE_FILTER),
}}
cachePolicy={"memory-disk"}
contentFit='contain'
style={{
aspectRatio: "4/3",
}}
/>
}
renderItem={(item, index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
/>
);
}

View File

@@ -0,0 +1,104 @@
import { Text } from "@/components/common/Text";
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import Poster from "@/components/posters/Poster";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import {
type MovieResult,
Results,
type TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { useInfiniteQuery } from "@tanstack/react-query";
import { router, useLocalSearchParams, useSegments } from "expo-router";
import { uniqBy } from "lodash";
import React, { useMemo } from "react";
import { TouchableOpacity } from "react-native";
export default function page() {
const local = useLocalSearchParams();
const { jellyseerrApi } = useJellyseerr();
const { genreId, name, type } = local as unknown as {
genreId: string;
name: string;
type: DiscoverSliderType;
};
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "company", type, genreId],
queryFn: async ({ pageParam }) => {
const params: any = {
page: Number(pageParam),
genre: genreId,
};
return jellyseerrApi?.discover(
type === DiscoverSliderType.MOVIE_GENRES
? Endpoints.DISCOVER_MOVIES
: Endpoints.DISCOVER_TV,
params,
);
},
enabled: !!jellyseerrApi && !!genreId,
initialPageParam: 1,
getNextPageParam: (lastPage, pages) =>
(lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
1,
staleTime: 0,
});
const flatData = useMemo(
() =>
uniqBy(
data?.pages
?.filter((p) => p?.results.length)
.flatMap((p) => p?.results ?? []),
"id",
) ?? [],
[data],
);
const backdrops = useMemo(
() =>
jellyseerrApi
? flatData.map((r) =>
jellyseerrApi.imageProxy(
(r as TvResult | MovieResult).backdropPath,
"w1920_and_h800_multi_faces",
),
)
: [],
[jellyseerrApi, flatData],
);
return (
<ParallaxSlideShow
data={flatData}
images={backdrops}
listHeader=''
keyExtractor={(item) => item.id.toString()}
onEndReached={() => {
if (hasNextPage) {
fetchNextPage();
}
}}
logo={
<Text
className='text-4xl font-bold text-center bottom-1'
style={{
...textShadowStyle.shadow,
shadowRadius: 10,
}}
>
{name}
</Text>
}
renderItem={(item, index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
/>
);
}

View File

@@ -0,0 +1,424 @@
import { Button } from "@/components/Button";
import { GenreTags } from "@/components/GenreTags";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { JellyserrRatings } from "@/components/Ratings";
import { Text } from "@/components/common/Text";
import Cast from "@/components/jellyseerr/Cast";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
import { ItemActions } from "@/components/series/SeriesActions";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
import {
type IssueType,
IssueTypeName,
} from "@/utils/jellyseerr/server/constants/issue";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation, useRouter } from "expo-router";
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import RequestModal from "@/components/jellyseerr/RequestModal";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
const Page: React.FC = () => {
const insets = useSafeAreaInsets();
const params = useLocalSearchParams();
const { t } = useTranslation();
const router = useRouter();
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
params as unknown as {
mediaTitle: string;
releaseYear: number;
canRequest: string;
posterSrc: string;
mediaType: MediaType;
} & Partial<MovieResult | TvResult | MovieDetails | TvDetails>;
const navigation = useNavigation();
const { jellyseerrApi, requestMedia } = useJellyseerr();
const [issueType, setIssueType] = useState<IssueType>();
const [issueMessage, setIssueMessage] = useState<string>();
const [requestBody, _setRequestBody] = useState<MediaRequestBody>();
const advancedReqModalRef = useRef<BottomSheetModal>(null);
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const {
data: details,
isFetching,
isLoading,
refetch,
} = useQuery({
enabled: !!jellyseerrApi && !!result && !!result.id,
queryKey: ["jellyseerr", "detail", mediaType, result.id],
staleTime: 0,
refetchOnMount: true,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
retryOnMount: true,
refetchInterval: 0,
queryFn: async () => {
return mediaType === MediaType.MOVIE
? jellyseerrApi?.movieDetails(result.id!)
: jellyseerrApi?.tvDetails(result.id!);
},
});
const [canRequest, hasAdvancedRequestPermission] =
useJellyseerrCanRequest(details);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const submitIssue = useCallback(() => {
if (result.id && issueType && issueMessage && details) {
jellyseerrApi
?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage)
.then(() => {
setIssueType(undefined);
setIssueMessage(undefined);
bottomSheetModalRef?.current?.close();
});
}
}, [jellyseerrApi, details, result, issueType, issueMessage]);
const setRequestBody = useCallback(
(body: MediaRequestBody) => {
_setRequestBody(body);
advancedReqModalRef?.current?.present?.();
},
[requestBody, _setRequestBody, advancedReqModalRef],
);
const request = useCallback(async () => {
const body: MediaRequestBody = {
mediaId: Number(result.id!),
mediaType: mediaType!,
tvdbId: details?.externalIds?.tvdbId,
seasons: (details as TvDetails)?.seasons
?.filter?.((s) => s.seasonNumber !== 0)
?.map?.((s) => s.seasonNumber),
};
if (hasAdvancedRequestPermission) {
setRequestBody(body);
return;
}
requestMedia(mediaTitle, body, refetch);
}, [details, result, requestMedia, hasAdvancedRequestPermission]);
const isAnime = useMemo(
() =>
(details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) &&
mediaType === MediaType.TV,
[details],
);
useEffect(() => {
if (details) {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity className='rounded-full p-2 bg-neutral-800/80'>
<ItemActions item={details} />
</TouchableOpacity>
),
});
}
}, [details]);
return (
<View
className='flex-1 relative'
style={{
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<ParallaxScrollView
className='flex-1 opacity-100'
headerHeight={300}
headerImage={
<View>
{result.backdropPath ? (
<Image
cachePolicy={"memory-disk"}
transition={300}
style={{
width: "100%",
height: "100%",
}}
source={{
uri: jellyseerrApi?.imageProxy(
result.backdropPath,
"w1920_and_h800_multi_faces",
),
}}
/>
) : (
<View
style={{
width: "100%",
height: "100%",
}}
className='flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900'
>
<Ionicons
name='image-outline'
size={24}
color='white'
style={{ opacity: 0.4 }}
/>
</View>
)}
</View>
}
>
<View className='flex flex-col'>
<View className='space-y-4'>
<View className='px-4'>
<View className='flex flex-row justify-between w-full'>
<View className='flex flex-col w-56'>
<JellyserrRatings
result={
result as
| MovieResult
| TvResult
| MovieDetails
| TvDetails
}
/>
<Text
uiTextView
selectable
className='font-bold text-2xl mb-1'
>
{mediaTitle}
</Text>
<Text className='opacity-50'>{releaseYear}</Text>
</View>
<Image
className='absolute bottom-1 right-1 rounded-lg w-28 aspect-[10/15] border-2 border-neutral-800/50 drop-shadow-2xl'
cachePolicy={"memory-disk"}
transition={300}
source={{
uri: posterSrc,
}}
/>
</View>
<View>
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
</View>
{isLoading || isFetching ? (
<Button
loading={true}
disabled={true}
color='purple'
className='mt-4'
/>
) : canRequest ? (
<Button color='purple' onPress={request} className='mt-4'>
{t("jellyseerr.request_button")}
</Button>
) : (
details?.mediaInfo?.jellyfinMediaId && (
<View className='flex flex-row space-x-2 mt-4'>
<Button
className='flex-1 bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100'
color='transparent'
onPress={() => bottomSheetModalRef?.current?.present()}
iconLeft={
<Ionicons
name='warning-outline'
size={20}
color='white'
/>
}
style={{
borderWidth: 1,
borderStyle: "solid",
}}
>
<Text className='text-sm'>
{t("jellyseerr.report_issue_button")}
</Text>
</Button>
<Button
className='flex-1 bg-purple-600/50 border-purple-400 ring-purple-400 text-purple-100'
onPress={() => {
const url =
mediaType === MediaType.MOVIE
? `/(auth)/(tabs)/(search)/items/page?id=${details?.mediaInfo.jellyfinMediaId}`
: `/(auth)/(tabs)/(search)/series/${details?.mediaInfo.jellyfinMediaId}`;
// @ts-expect-error
router.push(url);
}}
iconLeft={
<Ionicons name='play-outline' size={20} color='white' />
}
style={{
borderWidth: 1,
borderStyle: "solid",
}}
>
<Text className='text-sm'>Play</Text>
</Button>
</View>
)
)}
<OverviewText text={result.overview} className='mt-4' />
</View>
{mediaType === MediaType.TV && (
<JellyseerrSeasons
isLoading={isLoading || isFetching}
details={details as TvDetails}
refetch={refetch}
hasAdvancedRequest={hasAdvancedRequestPermission}
onAdvancedRequest={(data) => setRequestBody(data)}
/>
)}
<DetailFacts
className='p-2 border border-neutral-800 bg-neutral-900 rounded-xl'
details={details}
/>
<Cast details={details} />
</View>
</View>
</ParallaxScrollView>
<RequestModal
ref={advancedReqModalRef}
requestBody={requestBody}
title={mediaTitle}
id={result.id!}
type={mediaType}
isAnime={isAnime}
onRequested={() => {
_setRequestBody(undefined);
advancedReqModalRef?.current?.close();
refetch();
}}
onDismiss={() => _setRequestBody(undefined)}
/>
<BottomSheetModal
ref={bottomSheetModalRef}
enableDynamicSizing
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
backdropComponent={renderBackdrop}
>
<BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
<View>
<Text className='font-bold text-2xl text-neutral-100'>
{t("jellyseerr.whats_wrong")}
</Text>
</View>
<View className='flex flex-col space-y-2 items-start'>
<View className='flex flex-col'>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<View className='flex flex-col'>
<Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.issue_type")}
</Text>
<TouchableOpacity className='bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between'>
<Text style={{}} className='' numberOfLines={1}>
{issueType
? IssueTypeName[issueType]
: t("jellyseerr.select_an_issue")}
</Text>
</TouchableOpacity>
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={false}
side='bottom'
align='center'
alignOffset={0}
avoidCollisions={true}
collisionPadding={0}
sideOffset={0}
>
<DropdownMenu.Label>
{t("jellyseerr.types")}
</DropdownMenu.Label>
{Object.entries(IssueTypeName)
.reverse()
.map(([key, value], idx) => (
<DropdownMenu.Item
key={value}
onSelect={() =>
setIssueType(key as unknown as IssueType)
}
>
<DropdownMenu.ItemTitle>
{value}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
<View className='p-4 border border-neutral-800 rounded-xl bg-neutral-900 w-full'>
<BottomSheetTextInput
multiline
maxLength={254}
style={{ color: "white" }}
clearButtonMode='always'
placeholder={t("jellyseerr.describe_the_issue")}
placeholderTextColor='#9CA3AF'
// Issue with multiline + Textinput inside a portal
// https://github.com/callstack/react-native-paper/issues/1668
defaultValue={issueMessage}
onChangeText={setIssueMessage}
/>
</View>
</View>
<Button className='mt-auto' onPress={submitIssue} color='purple'>
{t("jellyseerr.submit_button")}
</Button>
</View>
</BottomSheetView>
</BottomSheetModal>
</View>
);
};
export default Page;

View File

@@ -0,0 +1,115 @@
import { OverviewText } from "@/components/OverviewText";
import { Text } from "@/components/common/Text";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams, useSegments } from "expo-router";
import { orderBy, uniqBy } from "lodash";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
export default function page() {
const local = useLocalSearchParams();
const { t } = useTranslation();
const {
jellyseerrApi,
jellyseerrUser,
jellyseerrRegion: region,
jellyseerrLocale: locale,
} = useJellyseerr();
const { personId } = local as { personId: string };
const { data, isLoading, isFetching } = useQuery({
queryKey: ["jellyseerr", "person", personId],
queryFn: async () => ({
details: await jellyseerrApi?.personDetails(personId),
combinedCredits: await jellyseerrApi?.personCombinedCredits(personId),
}),
enabled: !!jellyseerrApi && !!personId,
});
const castedRoles: PersonCreditCast[] = useMemo(
() =>
uniqBy(
orderBy(
data?.combinedCredits?.cast,
["voteCount", "voteAverage"],
"desc",
),
"id",
),
[data?.combinedCredits],
);
const backdrops = useMemo(
() =>
jellyseerrApi
? castedRoles.map((c) =>
jellyseerrApi.imageProxy(
c.backdropPath,
"w1920_and_h800_multi_faces",
),
)
: [],
[jellyseerrApi, data?.combinedCredits],
);
return (
<ParallaxSlideShow
data={castedRoles}
images={backdrops}
listHeader={t("jellyseerr.appearances")}
keyExtractor={(item) => item.id.toString()}
logo={
<Image
key={data?.details?.id}
id={data?.details?.id.toString()}
className='rounded-full bottom-1'
source={{
uri: jellyseerrApi?.imageProxy(
data?.details?.profilePath,
"w600_and_h600_bestv2",
),
}}
cachePolicy={"memory-disk"}
contentFit='cover'
style={{
width: 125,
height: 125,
}}
/>
}
HeaderContent={() => (
<>
<Text className='font-bold text-2xl mb-1'>{data?.details?.name}</Text>
<Text className='opacity-50'>
{t("jellyseerr.born")}{" "}
{new Date(data?.details?.birthday!).toLocaleDateString(
`${locale}-${region}`,
{
year: "numeric",
month: "long",
day: "numeric",
},
)}{" "}
| {data?.details?.placeOfBirth}
</Text>
</>
)}
MainContent={() => (
<OverviewText text={data?.details?.biography} className='mt-4' />
)}
renderItem={(item, index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
/>
);
}

View File

@@ -0,0 +1,52 @@
import type {
MaterialTopTabNavigationEventMap,
MaterialTopTabNavigationOptions,
} from "@react-navigation/material-top-tabs";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
import type {
ParamListBase,
TabNavigationState,
} from "@react-navigation/native";
import { Stack, withLayoutContext } from "expo-router";
import React from "react";
const { Navigator } = createMaterialTopTabNavigator();
export const Tab = withLayoutContext<
MaterialTopTabNavigationOptions,
typeof Navigator,
TabNavigationState<ParamListBase>,
MaterialTopTabNavigationEventMap
>(Navigator);
const Layout = () => {
return (
<>
<Stack.Screen options={{ title: "Live TV" }} />
<Tab
initialRouteName='programs'
keyboardDismissMode='none'
screenOptions={{
tabBarBounces: true,
tabBarLabelStyle: { fontSize: 10 },
tabBarItemStyle: {
width: 100,
},
tabBarStyle: { backgroundColor: "black" },
animationEnabled: true,
lazy: true,
swipeEnabled: true,
tabBarIndicatorStyle: { backgroundColor: "#9334E9" },
tabBarScrollEnabled: true,
}}
>
<Tab.Screen name='programs' />
<Tab.Screen name='guide' />
<Tab.Screen name='channels' />
<Tab.Screen name='recordings' />
</Tab>
</>
);
};
export default Layout;

View File

@@ -0,0 +1,56 @@
import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import React from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const { data: channels } = useQuery({
queryKey: ["livetv", "channels"],
queryFn: async () => {
const res = await getLiveTvApi(api!).getLiveTvChannels({
startIndex: 0,
limit: 500,
enableFavoriteSorting: true,
userId: user?.Id,
addCurrentProgram: false,
enableUserData: false,
enableImageTypes: ["Primary"],
});
return res.data;
},
});
return (
<View className='flex flex-1'>
<FlashList
data={channels?.Items}
estimatedItemSize={76}
renderItem={({ item }) => (
<View className='flex flex-row items-center px-4 mb-2'>
<View className='w-22 mr-4 rounded-lg overflow-hidden'>
<ItemImage
style={{
aspectRatio: "1/1",
width: 60,
borderRadius: 8,
}}
item={item}
/>
</View>
<Text className='font-bold'>{item.Name}</Text>
</View>
)}
/>
</View>
);
}

View File

@@ -0,0 +1,221 @@
import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { HourHeader } from "@/components/livetv/HourHeader";
import { LiveTVGuideRow } from "@/components/livetv/LiveTVGuideRow";
import { TAB_HEIGHT } from "@/constants/Values";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dimensions,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const HOUR_HEIGHT = 30;
const ITEMS_PER_PAGE = 20;
const MemoizedLiveTVGuideRow = React.memo(LiveTVGuideRow);
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const [date, setDate] = useState<Date>(new Date());
const [currentPage, setCurrentPage] = useState(1);
const { data: guideInfo } = useQuery({
queryKey: ["livetv", "guideInfo"],
queryFn: async () => {
const res = await getLiveTvApi(api!).getGuideInfo();
return res.data;
},
});
const { data: channels } = useQuery({
queryKey: ["livetv", "channels", currentPage],
queryFn: async () => {
const res = await getLiveTvApi(api!).getLiveTvChannels({
startIndex: (currentPage - 1) * ITEMS_PER_PAGE,
limit: ITEMS_PER_PAGE,
enableFavoriteSorting: true,
userId: user?.Id,
addCurrentProgram: false,
enableUserData: false,
enableImageTypes: ["Primary"],
});
return res.data;
},
});
const { data: programs } = useQuery({
queryKey: ["livetv", "programs", date, currentPage],
queryFn: async () => {
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const now = new Date();
const isToday = startOfDay.toDateString() === now.toDateString();
const res = await getLiveTvApi(api!).getPrograms({
getProgramsDto: {
MaxStartDate: endOfDay.toISOString(),
MinEndDate: isToday ? now.toISOString() : startOfDay.toISOString(),
ChannelIds: channels?.Items?.map((c) => c.Id).filter(
Boolean,
) as string[],
ImageTypeLimit: 1,
EnableImages: false,
SortBy: ["StartDate"],
EnableTotalRecordCount: false,
EnableUserData: false,
},
});
return res.data;
},
enabled: !!channels,
});
const screenWidth = Dimensions.get("window").width;
const [scrollX, setScrollX] = useState(0);
const handleNextPage = useCallback(() => {
setCurrentPage((prev) => prev + 1);
}, []);
const handlePrevPage = useCallback(() => {
setCurrentPage((prev) => Math.max(1, prev - 1));
}, []);
return (
<ScrollView
nestedScrollEnabled
contentInsetAdjustmentBehavior='automatic'
key={"home"}
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
>
<PageButtons
currentPage={currentPage}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
isNextDisabled={
!channels || (channels?.Items?.length || 0) < ITEMS_PER_PAGE
}
/>
<View className='flex flex-row'>
<View className='flex flex-col w-[64px]'>
<View
style={{
height: HOUR_HEIGHT,
}}
className='bg-neutral-800'
/>
{channels?.Items?.map((c, i) => (
<View className='h-16 w-16 mr-4 rounded-lg overflow-hidden' key={i}>
<ItemImage
style={{
width: "100%",
height: "100%",
resizeMode: "contain",
}}
item={c}
/>
</View>
))}
</View>
<ScrollView
style={{
width: screenWidth - 64,
}}
horizontal
scrollEnabled
onScroll={(e) => {
setScrollX(e.nativeEvent.contentOffset.x);
}}
>
<View className='flex flex-col'>
<HourHeader height={HOUR_HEIGHT} />
{channels?.Items?.map((c, i) => (
<MemoizedLiveTVGuideRow
channel={c}
programs={programs?.Items}
key={c.Id}
scrollX={scrollX}
/>
))}
</View>
</ScrollView>
</View>
</ScrollView>
);
}
interface PageButtonsProps {
currentPage: number;
onPrevPage: () => void;
onNextPage: () => void;
isNextDisabled: boolean;
}
const PageButtons: React.FC<PageButtonsProps> = ({
currentPage,
onPrevPage,
onNextPage,
isNextDisabled,
}) => {
const { t } = useTranslation();
return (
<View className='flex flex-row justify-between items-center bg-neutral-800 w-full px-4 py-2'>
<TouchableOpacity
onPress={onPrevPage}
disabled={currentPage === 1}
className='flex flex-row items-center'
>
<Ionicons
name='chevron-back'
size={24}
color={currentPage === 1 ? "gray" : "white"}
/>
<Text
className={`ml-1 ${
currentPage === 1 ? "text-gray-500" : "text-white"
}`}
>
{t("live_tv.previous")}
</Text>
</TouchableOpacity>
<Text className='text-white'>Page {currentPage}</Text>
<TouchableOpacity
onPress={onNextPage}
disabled={isNextDisabled}
className='flex flex-row items-center'
>
<Text
className={`mr-1 ${isNextDisabled ? "text-gray-500" : "text-white"}`}
>
{t("live_tv.next")}
</Text>
<Ionicons
name='chevron-forward'
size={24}
color={isNextDisabled ? "gray" : "white"}
/>
</TouchableOpacity>
</View>
);
};

View File

@@ -0,0 +1,147 @@
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
import { TAB_HEIGHT } from "@/constants/Values";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtom } from "jotai";
import React from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const { t } = useTranslation();
return (
<ScrollView
nestedScrollEnabled
contentInsetAdjustmentBehavior='automatic'
key={"home"}
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
paddingTop: 8,
}}
>
<View className='flex flex-col space-y-2'>
<ScrollingCollectionList
queryKey={["livetv", "recommended"]}
title={t("live_tv.on_now")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getRecommendedPrograms({
userId: user?.Id,
isAiring: true,
limit: 24,
imageTypeLimit: 1,
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
enableTotalRecordCount: false,
fields: ["ChannelInfo", "PrimaryImageAspectRatio"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "shows"]}
title={t("live_tv.shows")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isMovie: false,
isSeries: true,
isSports: false,
isNews: false,
isKids: false,
enableTotalRecordCount: false,
fields: ["ChannelInfo", "PrimaryImageAspectRatio"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "movies"]}
title={t("live_tv.movies")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isMovie: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "sports"]}
title={t("live_tv.sports")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isSports: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "kids"]}
title={t("live_tv.for_kids")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isKids: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "news"]}
title={t("live_tv.news")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isNews: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
</View>
</ScrollView>
);
}

View File

@@ -0,0 +1,13 @@
import { Text } from "@/components/common/Text";
import React from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
export default function page() {
const { t } = useTranslation();
return (
<View className='flex items-center justify-center h-full -mt-12'>
<Text>{t("live_tv.coming_soon")}</Text>
</View>
);
}

View File

@@ -0,0 +1,153 @@
import { AddToFavorites } from "@/components/AddToFavorites";
import { DownloadItems } from "@/components/DownloadItem";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { SeriesHeader } from "@/components/series/SeriesHeader";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { Ionicons } from "@expo/vector-icons";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import type React from "react";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
const page: React.FC = () => {
const navigation = useNavigation();
const { t } = useTranslation();
const params = useLocalSearchParams();
const { id: seriesId, seasonIndex } = params as {
id: string;
seasonIndex: string;
};
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: item } = useQuery({
queryKey: ["series", seriesId],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: seriesId,
}),
staleTime: 60 * 1000,
});
const backdropUrl = useMemo(
() =>
getBackdropUrl({
api,
item,
quality: 90,
width: 1000,
}),
[item],
);
const logoUrl = useMemo(
() =>
getLogoImageUrlById({
api,
item,
}),
[item],
);
const { data: allEpisodes, isLoading } = useQuery({
queryKey: ["AllEpisodes", item?.Id],
queryFn: async () => {
const res = await getTvShowsApi(api!).getEpisodes({
seriesId: item?.Id!,
userId: user?.Id!,
enableUserData: true,
fields: ["MediaSources", "MediaStreams", "Overview"],
});
return res?.data.Items || [];
},
staleTime: 60,
enabled: !!api && !!user?.Id && !!item?.Id,
});
useEffect(() => {
navigation.setOptions({
headerRight: () =>
!isLoading &&
item &&
allEpisodes &&
allEpisodes.length > 0 && (
<View className='flex flex-row items-center space-x-2'>
<AddToFavorites item={item} />
{!Platform.isTV && (
<DownloadItems
size='large'
title={t("item_card.download.download_series")}
items={allEpisodes || []}
MissingDownloadIconComponent={() => (
<Ionicons name='download' size={22} color='white' />
)}
DownloadedIconComponent={() => (
<Ionicons
name='checkmark-done-outline'
size={24}
color='#9333ea'
/>
)}
/>
)}
</View>
),
});
}, [allEpisodes, isLoading, item]);
if (!item || !backdropUrl) return null;
return (
<ParallaxScrollView
headerHeight={400}
headerImage={
<Image
source={{
uri: backdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
}
logo={
logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null
}
>
<View className='flex flex-col pt-4'>
<SeriesHeader item={item} />
<View className='mb-4'>
<NextUp seriesId={seriesId} />
</View>
<SeasonPicker item={item} initialSeasonIndex={Number(seasonIndex)} />
</View>
</ParallaxScrollView>
);
};
export default page;

View File

@@ -0,0 +1,485 @@
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo } from "react";
import { FlatList, View, useWindowDimensions } from "react-native";
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemPoster } from "@/components/posters/ItemPoster";
import { useOrientation } from "@/hooks/useOrientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
SortByOption,
SortOrderOption,
genreFilterAtom,
getSortByPreference,
getSortOrderPreference,
sortByAtom,
sortByPreferenceAtom,
sortOptions,
sortOrderAtom,
sortOrderOptions,
sortOrderPreferenceAtom,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import type {
BaseItemDto,
BaseItemDtoQueryResult,
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import {
getFilterApi,
getItemsApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useTranslation } from "react-i18next";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const Page = () => {
const searchParams = useLocalSearchParams();
const { libraryId } = searchParams as { libraryId: string };
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { width: screenWidth } = useWindowDimensions();
const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom);
const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom);
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
const [sortBy, _setSortBy] = useAtom(sortByAtom);
const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom);
const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom);
const [sortOrderPreference, setOderByPreference] = useAtom(
sortOrderPreferenceAtom,
);
const { orientation } = useOrientation();
const { t } = useTranslation();
useEffect(() => {
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
if (sop) {
_setSortOrder([sop]);
} else {
_setSortOrder([SortOrderOption.Ascending]);
}
const obp = getSortByPreference(libraryId, sortByPreference);
if (obp) {
_setSortBy([obp]);
} else {
_setSortBy([SortByOption.SortName]);
}
}, []);
const setSortBy = useCallback(
(sortBy: SortByOption[]) => {
const sop = getSortByPreference(libraryId, sortByPreference);
if (sortBy[0] !== sop) {
setSortByPreference({ ...sortByPreference, [libraryId]: sortBy[0] });
}
_setSortBy(sortBy);
},
[libraryId, sortByPreference],
);
const setSortOrder = useCallback(
(sortOrder: SortOrderOption[]) => {
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
if (sortOrder[0] !== sop) {
setOderByPreference({
...sortOrderPreference,
[libraryId]: sortOrder[0],
});
}
_setSortOrder(sortOrder);
},
[libraryId, sortOrderPreference],
);
const nrOfCols = useMemo(() => {
if (screenWidth < 300) return 2;
if (screenWidth < 500) return 3;
if (screenWidth < 800) return 5;
if (screenWidth < 1000) return 6;
if (screenWidth < 1500) return 7;
return 6;
}, [screenWidth, orientation]);
const { data: library, isLoading: isLibraryLoading } = useQuery({
queryKey: ["library", libraryId],
queryFn: async () => {
if (!api) return null;
const response = await getUserLibraryApi(api).getItem({
itemId: libraryId,
userId: user?.Id,
});
return response.data;
},
enabled: !!api && !!user?.Id && !!libraryId,
staleTime: 60 * 1000,
});
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
title: library?.Name || "",
});
}, [library]);
const fetchItems = useCallback(
async ({
pageParam,
}: {
pageParam: number;
}): Promise<BaseItemDtoQueryResult | null> => {
if (!api || !library) return null;
let itemType: BaseItemKind | undefined;
// This fix makes sure to only return 1 type of items, if defined.
// This is because the underlying directory some times contains other types, and we don't want to show them.
if (library.CollectionType === "movies") {
itemType = "Movie";
} else if (library.CollectionType === "tvshows") {
itemType = "Series";
} else if (library.CollectionType === "boxsets") {
itemType = "BoxSet";
}
const response = await getItemsApi(api).getItems({
userId: user?.Id,
parentId: libraryId,
limit: 36,
startIndex: pageParam,
sortBy: [sortBy[0], "SortName", "ProductionYear"],
sortOrder: [sortOrder[0]],
enableImageTypes: ["Primary", "Backdrop", "Banner", "Thumb"],
// true is needed for merged versions
recursive: true,
imageTypeLimit: 1,
fields: ["PrimaryImageAspectRatio", "SortName"],
genres: selectedGenres,
tags: selectedTags,
years: selectedYears.map((year) => Number.parseInt(year)),
includeItemTypes: itemType ? [itemType] : undefined,
});
return response.data || null;
},
[
api,
user?.Id,
libraryId,
library,
selectedGenres,
selectedYears,
selectedTags,
sortBy,
sortOrder,
],
);
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
useInfiniteQuery({
queryKey: [
"library-items",
libraryId,
selectedGenres,
selectedYears,
selectedTags,
sortBy,
sortOrder,
],
queryFn: fetchItems,
getNextPageParam: (lastPage, pages) => {
if (
!lastPage?.Items ||
!lastPage?.TotalRecordCount ||
lastPage?.TotalRecordCount === 0
)
return undefined;
const totalItems = lastPage.TotalRecordCount;
const accumulatedItems = pages.reduce(
(acc, curr) => acc + (curr?.Items?.length || 0),
0,
);
if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length;
}
return undefined;
},
initialPageParam: 0,
enabled: !!api && !!user?.Id && !!library,
});
const flatData = useMemo(() => {
return (
(data?.pages.flatMap((p) => p?.Items).filter(Boolean) as BaseItemDto[]) ||
[]
);
}, [data]);
const renderItem = useCallback(
({ item, index }: { item: BaseItemDto; index: number }) => (
<TouchableItemRouter
key={item.Id}
style={{
width: "100%",
marginBottom: 4,
}}
item={item}
>
<View
style={{
alignSelf:
orientation === ScreenOrientation.OrientationLock.PORTRAIT_UP
? index % nrOfCols === 0
? "flex-end"
: (index + 1) % nrOfCols === 0
? "flex-start"
: "center"
: "center",
width: "89%",
}}
>
{/* <MoviePoster item={item} /> */}
<ItemPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableItemRouter>
),
[orientation],
);
const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []);
const ListHeaderComponent = useCallback(
() => (
<View className=''>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
display: "flex",
paddingHorizontal: 15,
paddingVertical: 16,
flexDirection: "row",
}}
data={[
{
key: "reset",
component: <ResetFiltersButton />,
},
{
key: "genre",
component: (
<FilterButton
className='mr-1'
id={libraryId}
queryKey='genreFilter'
queryFn={async () => {
if (!api) return null;
const response = await getFilterApi(
api,
).getQueryFiltersLegacy({
userId: user?.Id,
parentId: libraryId,
});
return response.data.Genres || [];
}}
set={setSelectedGenres}
values={selectedGenres}
title={t("library.filters.genres")}
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
{
key: "year",
component: (
<FilterButton
className='mr-1'
id={libraryId}
queryKey='yearFilter'
queryFn={async () => {
if (!api) return null;
const response = await getFilterApi(
api,
).getQueryFiltersLegacy({
userId: user?.Id,
parentId: libraryId,
});
return response.data.Years || [];
}}
set={setSelectedYears}
values={selectedYears}
title={t("library.filters.years")}
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) => item.includes(search)}
/>
),
},
{
key: "tags",
component: (
<FilterButton
className='mr-1'
id={libraryId}
queryKey='tagsFilter'
queryFn={async () => {
if (!api) return null;
const response = await getFilterApi(
api,
).getQueryFiltersLegacy({
userId: user?.Id,
parentId: libraryId,
});
return response.data.Tags || [];
}}
set={setSelectedTags}
values={selectedTags}
title={t("library.filters.tags")}
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
{
key: "sortBy",
component: (
<FilterButton
className='mr-1'
id={libraryId}
queryKey='sortBy'
queryFn={async () =>
sortOptions
.filter(
(s) =>
library?.CollectionType !== "movies" ||
s.key !== SortByOption.DateLastContentAdded,
)
.map((s) => s.key)
}
set={setSortBy}
values={sortBy}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
sortOptions.find((i) => i.key === item)?.value || ""
}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
{
key: "sortOrder",
component: (
<FilterButton
className='mr-1'
id={libraryId}
queryKey='sortOrder'
queryFn={async () => sortOrderOptions.map((s) => s.key)}
set={setSortOrder}
values={sortOrder}
title={t("library.filters.sort_order")}
renderItemLabel={(item) =>
sortOrderOptions.find((i) => i.key === item)?.value || ""
}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
),
},
]}
renderItem={({ item }) => item.component}
keyExtractor={(item) => item.key}
/>
</View>
),
[
libraryId,
api,
user?.Id,
selectedGenres,
setSelectedGenres,
selectedYears,
setSelectedYears,
selectedTags,
setSelectedTags,
sortBy,
setSortBy,
sortOrder,
setSortOrder,
isFetching,
],
);
const insets = useSafeAreaInsets();
if (isLoading || isLibraryLoading)
return (
<View className='w-full h-full flex items-center justify-center'>
<Loader />
</View>
);
return (
<FlashList
key={orientation}
ListEmptyComponent={
<View className='flex flex-col items-center justify-center h-full'>
<Text className='font-bold text-xl text-neutral-500'>
{t("library.no_results")}
</Text>
</View>
}
contentInsetAdjustmentBehavior='automatic'
data={flatData}
renderItem={renderItem}
extraData={[orientation, nrOfCols]}
keyExtractor={keyExtractor}
estimatedItemSize={244}
numColumns={nrOfCols}
onEndReached={() => {
if (hasNextPage) {
fetchNextPage();
}
}}
onEndReachedThreshold={1}
ListHeaderComponent={ListHeaderComponent}
contentContainerStyle={{
paddingBottom: 24,
paddingLeft: insets.left,
paddingRight: insets.right,
}}
ItemSeparatorComponent={() => (
<View
style={{
width: 10,
height: 10,
}}
/>
)}
/>
);
};
export default React.memo(Page);

View File

@@ -0,0 +1,222 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { Stack } from "expo-router";
import { Platform } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next";
export default function IndexLayout() {
const [settings, updateSettings, pluginSettings] = useSettings();
const { t } = useTranslation();
if (!settings?.libraryOptions) return null;
return (
<Stack>
<Stack.Screen
name='index'
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: t("tabs.library"),
headerBlurEffect: "prominent",
headerLargeStyle: {
backgroundColor: "black",
},
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
headerRight: () =>
!pluginSettings?.libraryOptions?.locked &&
!Platform.isTV && (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Ionicons
name='ellipsis-horizontal-outline'
size={24}
color='white'
/>
</DropdownMenu.Trigger>
<DropdownMenu.Content
align={"end"}
alignOffset={-10}
avoidCollisions={false}
collisionPadding={0}
loop={false}
side={"bottom"}
sideOffset={10}
>
<DropdownMenu.Label>
{t("library.options.display")}
</DropdownMenu.Label>
<DropdownMenu.Group key='display-group'>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger key='image-style-trigger'>
{t("library.options.display")}
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
alignOffset={-10}
avoidCollisions={true}
collisionPadding={0}
loop={true}
sideOffset={10}
>
<DropdownMenu.CheckboxItem
key='display-option-1'
value={settings.libraryOptions.display === "row"}
onValueChange={() =>
updateSettings({
libraryOptions: {
...settings.libraryOptions,
display: "row",
},
})
}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle key='display-title-1'>
{t("library.options.row")}
</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
key='display-option-2'
value={settings.libraryOptions.display === "list"}
onValueChange={() =>
updateSettings({
libraryOptions: {
...settings.libraryOptions,
display: "list",
},
})
}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle key='display-title-2'>
{t("library.options.list")}
</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger key='image-style-trigger'>
{t("library.options.image_style")}
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
alignOffset={-10}
avoidCollisions={true}
collisionPadding={0}
loop={true}
sideOffset={10}
>
<DropdownMenu.CheckboxItem
key='poster-option'
value={
settings.libraryOptions.imageStyle === "poster"
}
onValueChange={() =>
updateSettings({
libraryOptions: {
...settings.libraryOptions,
imageStyle: "poster",
},
})
}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle key='poster-title'>
{t("library.options.poster")}
</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
key='cover-option'
value={settings.libraryOptions.imageStyle === "cover"}
onValueChange={() =>
updateSettings({
libraryOptions: {
...settings.libraryOptions,
imageStyle: "cover",
},
})
}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle key='cover-title'>
{t("library.options.cover")}
</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Group>
<DropdownMenu.Group key='show-titles-group'>
<DropdownMenu.CheckboxItem
disabled={settings.libraryOptions.imageStyle === "poster"}
key='show-titles-option'
value={settings.libraryOptions.showTitles}
onValueChange={(newValue: string) => {
if (settings.libraryOptions.imageStyle === "poster")
return;
updateSettings({
libraryOptions: {
...settings.libraryOptions,
showTitles: newValue === "on",
},
});
}}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle key='show-titles-title'>
{t("library.options.show_titles")}
</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
key='show-stats-option'
value={settings.libraryOptions.showStats}
onValueChange={(newValue: string) => {
updateSettings({
libraryOptions: {
...settings.libraryOptions,
showStats: newValue === "on",
},
});
}}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle key='show-stats-title'>
{t("library.options.show_stats")}
</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
</DropdownMenu.Group>
<DropdownMenu.Separator />
</DropdownMenu.Content>
</DropdownMenu.Root>
),
}}
/>
<Stack.Screen
name='[libraryId]'
options={{
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}
<Stack.Screen
name='collections/[collectionId]'
options={{
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
</Stack>
);
}

View File

@@ -0,0 +1,111 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { LibraryItemCard } from "@/components/library/LibraryItemCard";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import {
getUserLibraryApi,
getUserViewsApi,
} from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function index() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const queryClient = useQueryClient();
const [settings] = useSettings();
const { t } = useTranslation();
const { data, isLoading } = useQuery({
queryKey: ["user-views", user?.Id],
queryFn: async () => {
const response = await getUserViewsApi(api!).getUserViews({
userId: user?.Id,
});
return response.data.Items || null;
},
staleTime: 60,
});
const libraries = useMemo(
() =>
data
?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!))
.filter((l) => l.CollectionType !== "music")
.filter((l) => l.CollectionType !== "books") || [],
[data, settings?.hiddenLibraries],
);
useEffect(() => {
for (const item of data || []) {
queryClient.prefetchQuery({
queryKey: ["library", item.Id],
queryFn: async () => {
if (!item.Id || !user?.Id || !api) return null;
const response = await getUserLibraryApi(api).getItem({
itemId: item.Id,
userId: user?.Id,
});
return response.data;
},
staleTime: 60 * 1000,
});
}
}, [data]);
const insets = useSafeAreaInsets();
if (isLoading)
return (
<View className='justify-center items-center h-full'>
<Loader />
</View>
);
if (!libraries)
return (
<View className='h-full w-full flex justify-center items-center'>
<Text className='text-lg text-neutral-500'>
{t("library.no_libraries_found")}
</Text>
</View>
);
return (
<FlashList
extraData={settings}
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingTop: 17,
paddingHorizontal: settings?.libraryOptions?.display === "row" ? 0 : 17,
paddingBottom: 150,
paddingLeft: insets.left,
paddingRight: insets.right,
}}
data={libraries}
renderItem={({ item }) => <LibraryItemCard library={item} />}
keyExtractor={(item) => item.Id || ""}
ItemSeparatorComponent={() =>
settings?.libraryOptions?.display === "row" ? (
<View
style={{
height: StyleSheet.hairlineWidth,
}}
className='bg-neutral-800 mx-2 my-4'
/>
) : (
<View className='h-4' />
)
}
estimatedItemSize={200}
/>
);
}

View File

@@ -0,0 +1,55 @@
import {
commonScreenOptions,
nestedTabPageScreenOptions,
} from "@/components/stacks/NestedTabPageStack";
import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
export default function SearchLayout() {
const { t } = useTranslation();
return (
<Stack>
<Stack.Screen
name='index'
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: t("tabs.search"),
headerLargeStyle: {
backgroundColor: "black",
},
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}
<Stack.Screen
name='collections/[collectionId]'
options={{
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
<Stack.Screen name='jellyseerr/page' options={commonScreenOptions} />
<Stack.Screen
name='jellyseerr/person/[personId]'
options={commonScreenOptions}
/>
<Stack.Screen
name='jellyseerr/company/[companyId]'
options={commonScreenOptions}
/>
<Stack.Screen
name='jellyseerr/genre/[genreId]'
options={commonScreenOptions}
/>
</Stack>
);
}

View File

@@ -0,0 +1,453 @@
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { Tag } from "@/components/GenreTags";
import { ItemCardText } from "@/components/ItemCardText";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import {
JellyseerrSearchSort,
JellyserrIndexPage,
} from "@/components/jellyseerr/JellyseerrIndexPage";
import MoviePoster from "@/components/posters/MoviePoster";
import SeriesPoster from "@/components/posters/SeriesPoster";
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import type {
BaseItemDto,
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDebounce } from "use-debounce";
type SearchType = "Library" | "Discover";
const exampleSearches = [
"Lord of the rings",
"Avengers",
"Game of Thrones",
"Breaking Bad",
"Stranger Things",
"The Mandalorian",
];
export default function search() {
const params = useLocalSearchParams();
const insets = useSafeAreaInsets();
const [user] = useAtom(userAtom);
const { t } = useTranslation();
const { q } = params as { q: string };
const [searchType, setSearchType] = useState<SearchType>("Library");
const [search, setSearch] = useState<string>("");
const [debouncedSearch] = useDebounce(search, 500);
const [api] = useAtom(apiAtom);
const [settings] = useSettings();
const { jellyseerrApi } = useJellyseerr();
const [jellyseerrOrderBy, setJellyseerrOrderBy] =
useState<JellyseerrSearchSort>(
JellyseerrSearchSort[
JellyseerrSearchSort.DEFAULT
] as unknown as JellyseerrSearchSort,
);
const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<
"asc" | "desc"
>("desc");
const searchEngine = useMemo(() => {
return settings?.searchEngine || "Jellyfin";
}, [settings]);
useEffect(() => {
if (q && q.length > 0) {
setSearch(q);
}
}, [q]);
const searchFn = useCallback(
async ({
types,
query,
}: {
types: BaseItemKind[];
query: string;
}): Promise<BaseItemDto[]> => {
if (!api || !query) {
return [];
}
try {
if (searchEngine === "Jellyfin") {
const searchApi = await getItemsApi(api).getItems({
searchTerm: query,
limit: 10,
includeItemTypes: types,
recursive: true,
userId: user?.Id,
});
return (searchApi.data.Items as BaseItemDto[]) || [];
}
if (!settings?.marlinServerUrl) {
return [];
}
const url = `${
settings.marlinServerUrl
}/search?q=${encodeURIComponent(query)}&includeItemTypes=${types
.map((type) => encodeURIComponent(type))
.join("&includeItemTypes=")}`;
const response1 = await axios.get(url);
const ids = response1.data.ids;
if (!ids || !ids.length) {
return [];
}
const response2 = await getItemsApi(api).getItems({
ids,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
});
return (response2.data.Items as BaseItemDto[]) || [];
} catch (error) {
console.error("Error during search:", error);
return []; // Ensure an empty array is returned in case of an error
}
},
[api, searchEngine, settings],
);
type HeaderSearchBarRef = {
focus: () => void;
blur: () => void;
setText: (text: string) => void;
clearText: () => void;
cancelSearch: () => void;
};
const searchBarRef = useRef<HeaderSearchBarRef>(null);
const navigation = useNavigation();
useLayoutEffect(() => {
navigation.setOptions({
headerSearchBarOptions: {
ref: searchBarRef,
placeholder: t("search.search"),
onChangeText: (e: any) => {
router.setParams({ q: "" });
setSearch(e.nativeEvent.text);
},
hideWhenScrolling: false,
autoFocus: false,
},
});
}, [navigation]);
useEffect(() => {
const unsubscribe = eventBus.on("searchTabPressed", () => {
// Screen not active
if (!searchBarRef.current) {
return;
}
// Screen is active, focus search bar
searchBarRef.current?.focus();
});
return () => {
unsubscribe();
};
}, []);
const { data: movies, isFetching: l1 } = useQuery({
queryKey: ["search", "movies", debouncedSearch],
queryFn: () =>
searchFn({
query: debouncedSearch,
types: ["Movie"],
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
const { data: series, isFetching: l2 } = useQuery({
queryKey: ["search", "series", debouncedSearch],
queryFn: () =>
searchFn({
query: debouncedSearch,
types: ["Series"],
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
const { data: episodes, isFetching: l3 } = useQuery({
queryKey: ["search", "episodes", debouncedSearch],
queryFn: () =>
searchFn({
query: debouncedSearch,
types: ["Episode"],
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
const { data: collections, isFetching: l7 } = useQuery({
queryKey: ["search", "collections", debouncedSearch],
queryFn: () =>
searchFn({
query: debouncedSearch,
types: ["BoxSet"],
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
const { data: actors, isFetching: l8 } = useQuery({
queryKey: ["search", "actors", debouncedSearch],
queryFn: () =>
searchFn({
query: debouncedSearch,
types: ["Person"],
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
const noResults = useMemo(() => {
return !(
movies?.length ||
episodes?.length ||
series?.length ||
collections?.length ||
actors?.length
);
}, [episodes, movies, series, collections, actors]);
const loading = useMemo(() => {
return l1 || l2 || l3 || l7 || l8;
}, [l1, l2, l3, l7, l8]);
return (
<>
<ScrollView
keyboardDismissMode='on-drag'
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<View
className='flex flex-col'
style={{
marginTop: Platform.OS === "android" ? 16 : 0,
}}
>
{jellyseerrApi && (
<ScrollView
horizontal
className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
>
<TouchableOpacity onPress={() => setSearchType("Library")}>
<Tag
text={t("search.library")}
textClass='p-1'
className={
searchType === "Library" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSearchType("Discover")}>
<Tag
text={t("search.discover")}
textClass='p-1'
className={
searchType === "Discover" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
{searchType === "Discover" &&
!loading &&
noResults &&
debouncedSearch.length > 0 && (
<View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton
id='search'
queryKey='jellyseerr_search'
queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) =>
Number.isNaN(Number(v)),
)
}
set={(value) => setJellyseerrOrderBy(value[0])}
values={[jellyseerrOrderBy]}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`)
}
showSearch={false}
/>
<FilterButton
id='order'
queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
values={[jellyseerrSortOrder]}
title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)}
showSearch={false}
/>
</View>
)}
</ScrollView>
)}
<View className='mt-2'>
<LoadingSkeleton isLoading={loading} />
</View>
{searchType === "Library" ? (
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
<SearchItemWrapper
header={t("search.movies")}
items={movies}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
className='flex flex-col w-28 mr-2'
item={item}
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
<Text className='opacity-50 text-xs'>
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={series}
header={t("search.series")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
item={item}
className='flex flex-col w-28 mr-2'
>
<SeriesPoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
<Text className='opacity-50 text-xs'>
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={episodes}
header={t("search.episodes")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
item={item}
key={item.Id}
className='flex flex-col w-44 mr-2'
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={collections}
header={t("search.collections")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
item={item}
className='flex flex-col w-28 mr-2'
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={actors}
header={t("search.actors")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
item={item}
key={item.Id}
className='flex flex-col w-28 mr-2'
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
</View>
) : (
<JellyserrIndexPage
searchQuery={debouncedSearch}
sortType={jellyseerrOrderBy}
order={jellyseerrSortOrder}
/>
)}
{searchType === "Library" &&
(!loading && noResults && debouncedSearch.length > 0 ? (
<View>
<Text className='text-center text-lg font-bold mt-4'>
{t("search.no_results_found_for")}
</Text>
<Text className='text-xs text-purple-600 text-center'>
"{debouncedSearch}"
</Text>
</View>
) : debouncedSearch.length === 0 ? (
<View className='mt-4 flex flex-col items-center space-y-2'>
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => {
setSearch(e);
searchBarRef.current?.setText(e);
}}
key={e}
className='mb-2'
>
<Text className='text-purple-600'>{e}</Text>
</TouchableOpacity>
))}
</View>
) : null)}
</View>
</ScrollView>
</>
);
}

View File

@@ -1,73 +1,151 @@
import { router, Tabs } from "expo-router";
import React, { useEffect } from "react";
import * as NavigationBar from "expo-navigation-bar";
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
import React, { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { useFocusEffect, useRouter, withLayoutContext } from "expo-router";
import {
type NativeBottomTabNavigationEventMap,
createNativeBottomTabNavigator,
} from "@bottom-tabs/react-navigation";
const { Navigator } = createNativeBottomTabNavigator();
import type { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
import { Colors } from "@/constants/Colors";
import { Platform, TouchableOpacity } from "react-native";
import { Feather } from "@expo/vector-icons";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import { storage } from "@/utils/mmkv";
import type {
ParamListBase,
TabNavigationState,
} from "@react-navigation/native";
import { SystemBars } from "react-native-edge-to-edge";
export const NativeTabs = withLayoutContext<
BottomTabNavigationOptions,
typeof Navigator,
TabNavigationState<ParamListBase>,
NativeBottomTabNavigationEventMap
>(Navigator);
export default function TabLayout() {
useEffect(() => {
if (Platform.OS === "android") {
NavigationBar.setBackgroundColorAsync("#121212");
NavigationBar.setBorderColorAsync("#121212");
}
}, []);
const [settings] = useSettings();
const { t } = useTranslation();
const router = useRouter();
useFocusEffect(
useCallback(() => {
const hasShownIntro = storage.getBoolean("hasShownIntro");
if (!hasShownIntro) {
const timer = setTimeout(() => {
router.push("/intro/page");
}, 1000);
return () => {
clearTimeout(timer);
};
}
}, []),
);
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors.tabIconSelected,
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
headerShown: true,
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
title: "Home",
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? "home" : "home-outline"}
color={color}
/>
),
headerLeft: () => (
<TouchableOpacity
style={{ marginHorizontal: 17 }}
onPress={() => {
router.push("/(auth)/downloads");
}}
>
<Feather name="download" color={"white"} size={24} />
</TouchableOpacity>
),
headerRight: () => (
<TouchableOpacity
style={{ marginHorizontal: 17 }}
onPress={() => {
router.push("/(auth)/settings");
}}
>
<Feather name="settings" color={"white"} size={24} />
</TouchableOpacity>
),
<>
<SystemBars hidden={false} style='light' />
<NativeTabs
sidebarAdaptable={false}
ignoresTopSafeArea
tabBarStyle={{
backgroundColor: "#121212",
}}
/>
<Tabs.Screen
name="search"
options={{
headerStyle: { backgroundColor: "black" },
headerShown: true,
headerShadowVisible: false,
title: "Search",
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? "search" : "search"} color={color} />
),
}}
/>
</Tabs>
tabBarActiveTintColor={Colors.primary}
scrollEdgeAppearance='default'
>
<NativeTabs.Screen redirect name='index' />
<NativeTabs.Screen
listeners={({ navigation }) => ({
tabPress: (e) => {
eventBus.emit("scrollToTop");
},
})}
name='(home)'
options={{
title: t("tabs.home"),
tabBarIcon:
Platform.OS === "android"
? ({ color, focused, size }) =>
require("@/assets/icons/house.fill.png")
: ({ focused }) =>
focused
? { sfSymbol: "house.fill" }
: { sfSymbol: "house" },
}}
/>
<NativeTabs.Screen
listeners={({ navigation }) => ({
tabPress: (e) => {
eventBus.emit("searchTabPressed");
},
})}
name='(search)'
options={{
title: t("tabs.search"),
tabBarIcon:
Platform.OS === "android"
? ({ color, focused, size }) =>
require("@/assets/icons/magnifyingglass.png")
: ({ focused }) =>
focused
? { sfSymbol: "magnifyingglass" }
: { sfSymbol: "magnifyingglass" },
}}
/>
<NativeTabs.Screen
name='(favorites)'
options={{
title: t("tabs.favorites"),
tabBarIcon:
Platform.OS === "android"
? ({ color, focused, size }) =>
focused
? require("@/assets/icons/heart.fill.png")
: require("@/assets/icons/heart.png")
: ({ focused }) =>
focused
? { sfSymbol: "heart.fill" }
: { sfSymbol: "heart" },
}}
/>
<NativeTabs.Screen
name='(libraries)'
options={{
title: t("tabs.library"),
tabBarIcon:
Platform.OS === "android"
? ({ color, focused, size }) =>
require("@/assets/icons/server.rack.png")
: ({ focused }) =>
focused
? { sfSymbol: "rectangle.stack.fill" }
: { sfSymbol: "rectangle.stack" },
}}
/>
<NativeTabs.Screen
name='(custom-links)'
options={{
title: t("tabs.custom_links"),
// @ts-expect-error
tabBarItemHidden: !settings?.showCustomMenuLinks,
tabBarIcon:
Platform.OS === "android"
? ({ focused }) => require("@/assets/icons/list.png")
: ({ focused }) =>
focused
? { sfSymbol: "list.dash.fill" }
: { sfSymbol: "list.dash" },
}}
/>
</NativeTabs>
</>
);
}

View File

@@ -1,365 +1,5 @@
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { ItemCardText } from "@/components/ItemCardText";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
BaseItemDto,
ItemFields,
ItemFilter,
} from "@jellyfin/sdk/lib/generated-client/models";
import {
getChannelsApi,
getItemsApi,
getSuggestionsApi,
getTvShowsApi,
getUserApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
RefreshControl,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
import { Button } from "@/components/Button";
import { Ionicons } from "@expo/vector-icons";
import MoviePoster from "@/components/MoviePoster";
const Index = () => {
return null;
};
export default function index() {
const router = useRouter();
const queryClient = useQueryClient();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [loading, setLoading] = useState(false);
const { data, isLoading, isError } = useQuery<BaseItemDto[]>({
queryKey: ["resumeItems", user?.Id],
queryFn: async () =>
(api &&
(
await getItemsApi(api).getResumeItems({
userId: user?.Id,
})
).data.Items) ||
[],
enabled: !!api && !!user?.Id,
staleTime: 60,
});
const { data: _nextUpData } = useQuery({
queryKey: ["nextUp-all", user?.Id],
queryFn: async () =>
(api &&
(
await getTvShowsApi(api).getNextUp({
userId: user?.Id,
fields: ["MediaSourceCount"],
})
).data.Items) ||
[],
enabled: !!api && !!user?.Id,
staleTime: 0,
});
const nextUpData = useMemo(() => {
return _nextUpData?.filter((i) => !data?.find((d) => d.Id === i.Id));
}, [_nextUpData]);
const { data: collections } = useQuery({
queryKey: ["collections", user?.Id],
queryFn: async () => {
if (!api || !user?.Id) {
return [];
}
const data = (
await getItemsApi(api).getItems({
userId: user.Id,
})
).data;
const order = ["boxsets", "tvshows", "movies"];
const cs = data.Items?.sort((a, b) => {
if (
order.indexOf(a.CollectionType!) < order.indexOf(b.CollectionType!)
) {
return 1;
}
return -1;
});
return data.Items || [];
},
enabled: !!api && !!user?.Id,
staleTime: 0,
});
const movieCollectionId = useMemo(() => {
return collections?.find((c) => c.CollectionType === "movies")?.Id;
}, [collections]);
const tvShowCollectionId = useMemo(() => {
return collections?.find((c) => c.CollectionType === "tvshows")?.Id;
}, [collections]);
const { data: recentlyAddedInMovies } = useQuery<BaseItemDto[]>({
queryKey: ["recentlyAddedInMovies", user?.Id, movieCollectionId],
queryFn: async () =>
(api &&
(
await getUserLibraryApi(api).getLatestMedia({
userId: user?.Id,
limit: 50,
fields: ["PrimaryImageAspectRatio", "Path"],
imageTypeLimit: 1,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
parentId: movieCollectionId,
})
).data) ||
[],
enabled: !!api && !!user?.Id && !!movieCollectionId,
staleTime: 60,
});
const { data: recentlyAddedInTVShows } = useQuery<BaseItemDto[]>({
queryKey: ["recentlyAddedInTVShows", user?.Id, tvShowCollectionId],
queryFn: async () =>
(api &&
(
await getUserLibraryApi(api).getLatestMedia({
userId: user?.Id,
limit: 50,
fields: ["PrimaryImageAspectRatio", "Path"],
imageTypeLimit: 1,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
parentId: tvShowCollectionId,
})
).data) ||
[],
enabled: !!api && !!user?.Id && !!tvShowCollectionId,
staleTime: 60,
});
const { data: suggestions } = useQuery<BaseItemDto[]>({
queryKey: ["suggestions", user?.Id],
queryFn: async () =>
(api &&
(
await getSuggestionsApi(api).getSuggestions({
userId: user?.Id,
limit: 5,
mediaType: ["Video"],
})
).data.Items) ||
[],
enabled: !!api && !!user?.Id,
staleTime: 60,
});
const refetch = useCallback(async () => {
setLoading(true);
await queryClient.refetchQueries({ queryKey: ["resumeItems", user?.Id] });
await queryClient.refetchQueries({ queryKey: ["items", user?.Id] });
await queryClient.refetchQueries({ queryKey: ["suggestions", user?.Id] });
await queryClient.refetchQueries({ queryKey: ["recentlyAddedInMovies"] });
setLoading(false);
}, [queryClient, user?.Id]);
const [isConnected, setIsConnected] = useState<boolean | null>(null);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
if (state.isConnected == false || state.isInternetReachable === false)
setIsConnected(false);
else setIsConnected(true);
});
NetInfo.fetch().then((state) => {
setIsConnected(state.isConnected);
});
return () => {
unsubscribe();
};
}, []);
if (isConnected === false) {
return (
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
<Text className="text-3xl font-bold mb-2">No Internet</Text>
<Text className="text-center opacity-70">
No worries, you can still watch{"\n"}downloaded content.
</Text>
<View className="mt-4">
<Button
color="purple"
onPress={() => router.push("/(auth)/downloads")}
justify="center"
iconRight={
<Ionicons name="arrow-forward" size={20} color="white" />
}
>
Go to downloads
</Button>
</View>
</View>
);
}
if (isError)
return (
<View className="flex flex-col items-center justify-center h-full -mt-6">
<Text className="text-3xl font-bold mb-2">Oops!</Text>
<Text className="text-center opacity-70">
Something went wrong.{"\n"}Please log out and in again.
</Text>
</View>
);
if (isLoading)
return (
<View className="justify-center items-center h-full">
<ActivityIndicator />
</View>
);
return (
<ScrollView
nestedScrollEnabled
refreshControl={
<RefreshControl refreshing={loading} onRefresh={refetch} />
}
>
<View className="flex flex-col pt-4 pb-24 gap-y-4">
<View>
<Text className="px-4 text-2xl font-bold mb-2">
Continue Watching
</Text>
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
</View>
<View>
<Text className="px-4 text-2xl font-bold mb-2">Next Up</Text>
<HorizontalScroll<BaseItemDto>
data={nextUpData}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
</View>
<View>
<Text className="px-4 text-2xl font-bold mb-2">
Recently Added in Movies
</Text>
<HorizontalScroll<BaseItemDto>
data={recentlyAddedInMovies}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-32"
>
<View>
<MoviePoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
</View>
<View>
<Text className="px-4 text-2xl font-bold mb-2">
Recently Added in TV-Shows
</Text>
<HorizontalScroll<BaseItemDto>
data={recentlyAddedInTVShows}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/series/${item.Id}/page`)}
className="flex flex-col w-32"
>
<View>
<MoviePoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
</View>
<View>
<Text className="px-4 text-2xl font-bold mb-2">Collections</Text>
<HorizontalScroll<BaseItemDto>
data={collections}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/collections/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
</View>
<View>
<Text className="px-4 text-2xl font-bold mb-2">Suggestions</Text>
<HorizontalScroll<BaseItemDto>
data={suggestions}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableOpacity>
)}
/>
</View>
</View>
</ScrollView>
);
}
export default Index;

View File

@@ -1,193 +0,0 @@
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { ItemCardText } from "@/components/ItemCardText";
import MoviePoster from "@/components/MoviePoster";
import Poster from "@/components/Poster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
import React, { useState } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native";
export default function search() {
const [search, setSearch] = useState<string>("");
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: movies } = useQuery({
queryKey: ["search-movies", search],
queryFn: async () => {
if (!api || !user || search.length === 0) return [];
const searchApi = await getSearchApi(api).getSearchHints({
searchTerm: search,
limit: 10,
includeItemTypes: ["Movie"],
});
return searchApi.data.SearchHints;
},
});
const { data: series } = useQuery({
queryKey: ["search-series", search],
queryFn: async () => {
if (!api || !user || search.length === 0) return [];
const searchApi = await getSearchApi(api).getSearchHints({
searchTerm: search,
limit: 10,
includeItemTypes: ["Series"],
});
return searchApi.data.SearchHints;
},
});
const { data: episodes } = useQuery({
queryKey: ["search-episodes", search],
queryFn: async () => {
if (!api || !user || search.length === 0) return [];
const searchApi = await getSearchApi(api).getSearchHints({
searchTerm: search,
limit: 10,
includeItemTypes: ["Episode"],
});
return searchApi.data.SearchHints;
},
});
return (
<ScrollView keyboardDismissMode="on-drag">
<View className="flex flex-col pt-2 pb-20">
<View className="mb-4 px-4">
<Input
autoCorrect={false}
returnKeyType="done"
keyboardType="web-search"
placeholder="Search here..."
value={search}
onChangeText={(text) => setSearch(text)}
/>
</View>
<Text className="font-bold text-2xl px-4 mb-2">Movies</Text>
<SearchItemWrapper
ids={movies?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableOpacity
key={item.Id}
className="flex flex-col w-32"
onPress={() => router.push(`/items/${item.Id}/page`)}
>
<MoviePoster item={item} key={item.Id} />
<Text className="mt-2">{item.Name}</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableOpacity>
)}
/>
)}
/>
<Text className="font-bold text-2xl px-4 my-2">Series</Text>
<SearchItemWrapper
ids={series?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/series/${item.Id}/page`)}
className="flex flex-col w-32"
>
<Poster
item={item}
key={item.Id}
url={getPrimaryImageUrl({ api, item })}
/>
<Text className="mt-2">{item.Name}</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableOpacity>
)}
/>
)}
/>
<Text className="font-bold text-2xl px-4 my-2">Episodes</Text>
<SearchItemWrapper
ids={episodes?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableOpacity>
)}
/>
)}
/>
</View>
</ScrollView>
);
}
type Props = {
ids?: string[] | null;
renderItem: (data: BaseItemDto[]) => React.ReactNode;
};
const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data, isLoading: l1 } = useQuery({
queryKey: ["items", ids],
queryFn: async () => {
if (!user?.Id || !api || !ids || ids.length === 0) {
return [];
}
const itemPromises = ids.map((id) =>
getUserItemData({
api,
userId: user.Id,
itemId: id,
}),
);
const results = await Promise.all(itemPromises);
// Filter out null items
return results.filter(
(item) => item !== null,
) as unknown as BaseItemDto[];
},
enabled: !!ids && ids.length > 0 && !!api && !!user?.Id,
staleTime: Infinity,
});
if (!data) return <Text className="opacity-50 text-xs px-4">No results</Text>;
return renderItem(data);
};

View File

@@ -1,186 +0,0 @@
import { Text } from "@/components/common/Text";
import { Loading } from "@/components/Loading";
import MoviePoster from "@/components/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useMemo, useState } from "react";
import {
ActivityIndicator,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
const page: React.FC = () => {
const searchParams = useLocalSearchParams();
const { collection: collectionId } = searchParams as { collection: string };
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: collection } = useQuery({
queryKey: ["collection", collectionId],
queryFn: async () =>
(api &&
(
await getItemsApi(api).getItems({
userId: user?.Id,
})
).data.Items?.find((item) => item.Id == collectionId)) ||
null,
enabled: !!api && !!user?.Id,
staleTime: 0,
});
const [startIndex, setStartIndex] = useState<number>(0);
const { data, isLoading, isError } = useQuery<{
Items: BaseItemDto[];
TotalRecordCount: number;
}>({
queryKey: ["collection-items", collectionId, startIndex],
queryFn: async () => {
if (!api) return [];
const response = await api.axiosInstance.get(
`${api.basePath}/Users/${user?.Id}/Items`,
{
params: {
SortBy:
collection?.CollectionType === "movies"
? "SortName,ProductionYear"
: "SortName",
SortOrder: "Ascending",
IncludeItemTypes:
collection?.CollectionType === "movies" ? "Movie" : "Series",
Recursive: true,
Fields:
collection?.CollectionType === "movies"
? "PrimaryImageAspectRatio,MediaSourceCount"
: "PrimaryImageAspectRatio",
ImageTypeLimit: 1,
EnableImageTypes: "Primary,Backdrop,Banner,Thumb",
ParentId: collectionId,
Limit: 100,
StartIndex: startIndex,
},
headers: {
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
},
},
);
return response.data || [];
},
enabled: !!collection && !!api,
});
const totalItems = useMemo(() => {
return data?.TotalRecordCount;
}, [data]);
return (
<ScrollView>
<View>
<View className="px-4 mb-4">
<Text className="font-bold text-3xl mb-2">{collection?.Name}</Text>
<View className="flex flex-row items-center justify-between">
<Text>
{startIndex + 1}-{startIndex + 100} of {totalItems}
</Text>
<View className="flex flex-row items-center space-x-2">
<TouchableOpacity
onPress={() => {
setStartIndex((prev) => Math.max(prev - 100, 0));
}}
>
<Ionicons
name="arrow-back-circle-outline"
size={32}
color="white"
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setStartIndex((prev) => prev + 100);
}}
>
<Ionicons
name="arrow-forward-circle-outline"
size={32}
color="white"
/>
</TouchableOpacity>
</View>
</View>
</View>
{isLoading ? (
<View className="my-12">
<ActivityIndicator color={"white"} />
</View>
) : (
<View className="flex flex-row flex-wrap">
{data?.Items?.map((item: any, index: number) => (
<TouchableOpacity
style={{
maxWidth: "33%",
width: "100%",
padding: 10,
}}
key={index}
onPress={() => {
if (collection?.CollectionType === "movies") {
router.push(`/items/${item.Id}/page`);
} else if (collection?.CollectionType === "tvshows") {
router.push(`/series/${item.Id}/page`);
}
}}
>
<View className="flex flex-col gap-y-2">
<MoviePoster item={item} />
<Text>{item.Name}</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</View>
</TouchableOpacity>
))}
</View>
)}
</View>
{!isLoading && (
<View className="flex flex-row items-center space-x-2 justify-center mt-4 mb-12">
<TouchableOpacity
onPress={() => {
setStartIndex((prev) => Math.max(prev - 100, 0));
}}
>
<Ionicons
name="arrow-back-circle-outline"
size={32}
color="white"
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setStartIndex((prev) => prev + 100);
}}
>
<Ionicons
name="arrow-forward-circle-outline"
size={32}
color="white"
/>
</TouchableOpacity>
</View>
)}
</ScrollView>
);
};
export default page;

View File

@@ -1,148 +0,0 @@
import { Text } from "@/components/common/Text";
import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo } from "react";
import {
ActivityIndicator,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { useAtom } from "jotai";
import { runningProcesses } from "@/utils/atoms/downloads";
import { router } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { FFmpegKit } from "ffmpeg-kit-react-native";
const downloads: React.FC = () => {
const { data: downloadedFiles, isLoading } = useQuery({
queryKey: ["downloaded_files"],
queryFn: async () =>
JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) || "[]",
) as BaseItemDto[],
});
const movies = useMemo(
() => downloadedFiles?.filter((f) => f.Type === "Movie") || [],
[downloadedFiles],
);
const groupedBySeries = useMemo(() => {
const episodes = downloadedFiles?.filter((f) => f.Type === "Episode");
const series: { [key: string]: BaseItemDto[] } = {};
episodes?.forEach((e) => {
if (!series[e.SeriesName!]) series[e.SeriesName!] = [];
series[e.SeriesName!].push(e);
});
return Object.values(series);
}, [downloadedFiles]);
const [process, setProcess] = useAtom(runningProcesses);
const eta = useMemo(() => {
const length = process?.item?.RunTimeTicks || 0;
if (!process?.speed || !process?.progress) return "";
const timeLeft =
(length - length * (process.progress / 100)) / process.speed;
return formatNumber(timeLeft / 10000);
}, [process]);
if (isLoading) {
return (
<View className="h-full flex flex-col items-center justify-center -mt-6">
<ActivityIndicator size="small" color="white" />
</View>
);
}
return (
<ScrollView>
<View className="px-4 py-4">
<View className="mb-4">
<Text className="text-2xl font-bold mb-2">Active download</Text>
{process?.item ? (
<TouchableOpacity
onPress={() =>
router.push(`/(auth)/items/${process.item.Id}/page`)
}
className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between"
>
<View>
<Text className="font-semibold">{process.item.Name}</Text>
<Text className="text-xs opacity-50">{process.item.Type}</Text>
<View className="flex flex-row items-center space-x-2 mt-1 text-red-600">
<Text className="text-xs">
{process.progress.toFixed(0)}%
</Text>
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
<View>
<Text className="text-xs">ETA {eta}</Text>
</View>
</View>
</View>
<TouchableOpacity
onPress={() => {
FFmpegKit.cancel();
setProcess(null);
}}
>
<Ionicons name="close" size={24} color="red" />
</TouchableOpacity>
<View
className={`
absolute bottom-0 left-0 h-1 bg-red-600
`}
style={{
width: process.progress
? `${Math.max(5, process.progress)}%`
: "5%",
}}
></View>
</TouchableOpacity>
) : (
<Text className="opacity-50">No active downloads</Text>
)}
</View>
{movies.length > 0 && (
<View className="mb-4">
<View className="flex flex-row items-center justify-between mb-2">
<Text className="text-2xl font-bold">Movies</Text>
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
<Text className="text-xs font-bold">{movies?.length}</Text>
</View>
</View>
{movies?.map((item: BaseItemDto) => (
<View className="mb-2 last:mb-0" key={item.Id}>
<MovieCard item={item} />
</View>
))}
</View>
)}
{groupedBySeries?.map((items: BaseItemDto[], index: number) => (
<SeriesCard items={items} key={items[0].SeriesId} />
))}
</View>
</ScrollView>
);
};
export default downloads;
/*
* Format a number (Date.getTime) to a human readable string ex. 2m 34s
* @param {number} num - The number to format
*
* @returns {string} - The formatted string
*/
const formatNumber = (num: number) => {
const minutes = Math.floor(num / 60000);
const seconds = ((num % 60000) / 1000).toFixed(0);
return `${minutes}m ${seconds}s`;
};

View File

@@ -1,266 +0,0 @@
import { Chromecast } from "@/components/Chromecast";
import { Text } from "@/components/common/Text";
import { DownloadItem } from "@/components/DownloadItem";
import { PlayedStatus } from "@/components/PlayedStatus";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SimilarItems } from "@/components/SimilarItems";
import { VideoPlayer } from "@/components/VideoPlayer";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useMemo, useState } from "react";
import {
ActivityIndicator,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { ParallaxScrollView } from "../../../../components/ParallaxPage";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { PlayButton } from "@/components/PlayButton";
import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { useCastDevice } from "react-native-google-cast";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import ios12 from "@/utils/profiles/ios12";
import { currentlyPlayingItemAtom } from "@/components/CurrentlyPlayingBar";
const page: React.FC = () => {
const local = useLocalSearchParams();
const { id } = local as { id: string };
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const castDevice = useCastDevice();
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
key: "Max",
value: undefined,
});
const { data: item, isLoading: l1 } = useQuery({
queryKey: ["item", id],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: id,
}),
enabled: !!id && !!api,
staleTime: 60,
});
const backdropUrl = useMemo(
() =>
getBackdropUrl({
api,
item,
quality: 90,
width: 1000,
}),
[item],
);
const logoUrl = useMemo(
() => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null),
[item],
);
const { data: sessionData } = useQuery({
queryKey: ["sessionData", item?.Id],
queryFn: async () => {
if (!api || !user?.Id || !item?.Id) return null;
const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
itemId: item?.Id,
userId: user?.Id,
});
return playbackData.data;
},
enabled: !!item?.Id && !!api && !!user?.Id,
staleTime: 0,
});
const { data: playbackUrl } = useQuery({
queryKey: ["playbackUrl", item?.Id, maxBitrate, castDevice],
queryFn: async () => {
if (!api || !user?.Id || !sessionData) return null;
const url = await getStreamUrl({
api,
userId: user.Id,
item,
startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
maxStreamingBitrate: maxBitrate.value,
sessionData,
deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12,
});
return url;
},
enabled: !!sessionData,
staleTime: 0,
});
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
const onPressPlay = useCallback(() => {
if (!playbackUrl || !item) return;
setCp({
item,
playbackUrl,
});
}, [playbackUrl, item]);
if (l1)
return (
<View className="justify-center items-center h-full">
<ActivityIndicator />
</View>
);
if (!item?.Id || !backdropUrl) return null;
return (
<ParallaxScrollView
headerImage={
<Image
source={{
uri: backdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
}
logo={
<>
{logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null}
</>
}
>
<View className="flex flex-col px-4 mb-4 pt-4">
<View className="flex flex-col">
{item.Type === "Episode" ? (
<>
<TouchableOpacity
onPress={() =>
router.push(`/(auth)/series/${item.SeriesId}/page`)
}
>
<Text className="text-center opacity-50">
{item?.SeriesName}
</Text>
</TouchableOpacity>
<View className="flex flex-row items-center self-center px-4">
<Text className="text-center font-bold text-2xl mr-2">
{item?.Name}
</Text>
<PlayedStatus item={item} />
</View>
<View>
<View className="flex flex-row items-center self-center">
<TouchableOpacity onPress={() => {}}>
<Text className="text-center opacity-50">
{item?.SeasonName}
</Text>
</TouchableOpacity>
<Text className="text-center opacity-50 mx-2">{"—"}</Text>
<Text className="text-center opacity-50">
{`Episode ${item.IndexNumber}`}
</Text>
</View>
</View>
<Text className="text-center opacity-50">
{item.ProductionYear}
</Text>
</>
) : (
<>
<View className="flex flex-row items-center self-center px-4">
<Text className="text-center font-bold text-2xl mr-2">
{item?.Name}
</Text>
<PlayedStatus item={item} />
</View>
<Text className="text-center opacity-50">
{item?.ProductionYear}
</Text>
</>
)}
</View>
<View className="flex flex-row justify-between items-center w-full my-4">
{playbackUrl && (
<DownloadItem item={item} playbackUrl={playbackUrl} />
)}
<Chromecast />
</View>
<Text>{item.Overview}</Text>
</View>
<View className="flex flex-col p-4">
<BitrateSelector
onChange={(val) => setMaxBitrate(val)}
selected={maxBitrate}
/>
<PlayButton item={item} chromecastReady={false} onPress={onPressPlay} />
</View>
<ScrollView horizontal className="flex px-4 mb-4">
<View className="flex flex-row space-x-2 ">
<View className="flex flex-col">
<Text className="text-sm opacity-70">Video</Text>
<Text className="text-sm opacity-70">Audio</Text>
<Text className="text-sm opacity-70">Subtitles</Text>
</View>
<View className="flex flex-col">
<Text className="text-sm opacity-70">
{item.MediaStreams?.find((i) => i.Type === "Video")?.DisplayTitle}
</Text>
<Text className="text-sm opacity-70">
{item.MediaStreams?.find((i) => i.Type === "Audio")?.DisplayTitle}
</Text>
<Text className="text-sm opacity-70">
{
item.MediaStreams?.find((i) => i.Type === "Subtitle")
?.DisplayTitle
}
</Text>
</View>
</View>
</ScrollView>
<CastAndCrew item={item} />
{item.Type === "Episode" && (
<View className="mb-4">
<CurrentSeries item={item} />
</View>
)}
<SimilarItems itemId={item.Id} />
<View className="h-12"></View>
</ParallaxScrollView>
);
};
export default page;

View File

@@ -0,0 +1,47 @@
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useSettings } from "@/utils/atoms/settings";
import { Stack } from "expo-router";
import React, { useLayoutEffect } from "react";
import { Platform } from "react-native";
import { SystemBars } from "react-native-edge-to-edge";
export default function Layout() {
const [settings] = useSettings();
useLayoutEffect(() => {
if (Platform.isTV) return;
if (!settings.followDeviceOrientation && settings.defaultVideoOrientation) {
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
}
return () => {
if (Platform.isTV) return;
if (settings.followDeviceOrientation === true) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP,
);
}
};
});
return (
<>
<SystemBars hidden />
<Stack>
<Stack.Screen
name='direct-player'
options={{
headerShown: false,
autoHideHomeIndicator: true,
title: "",
animation: "fade",
}}
/>
</Stack>
</>
);
}

View File

@@ -0,0 +1,590 @@
import { BITRATES } from "@/components/BitrateSelector";
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { Controls } from "@/components/video-player/controls/Controls";
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useWebSocket } from "@/hooks/useWebsockets";
import { VlcPlayerView } from "@/modules";
import type {
PipStartedPayload,
PlaybackStatePayload,
ProgressUpdatePayload,
VlcPlayerViewRef,
} from "@/modules/VlcPlayer.types";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { writeToLog } from "@/utils/log";
import generateDeviceProfile from "@/utils/profiles/native";
import { msToTicks, ticksToSeconds } from "@/utils/time";
import {
type BaseItemDto,
type MediaSourceInfo,
PlaybackOrder,
type PlaybackProgressInfo,
PlaybackStartInfo,
RepeatMode,
} from "@jellyfin/sdk/lib/generated-client";
import {
getPlaystateApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { useGlobalSearchParams, useNavigation } from "expo-router";
import { useAtomValue } from "jotai";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Alert, Platform, View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const downloadProvider = !Platform.isTV
? require("@/providers/DownloadProvider")
: null;
export default function page() {
const videoRef = useRef<VlcPlayerViewRef>(null);
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
const { t } = useTranslation();
const navigation = useNavigation();
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
const [showControls, _setShowControls] = useState(true);
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [isBuffering, setIsBuffering] = useState(true);
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [isPipStarted, setIsPipStarted] = useState(false);
const progress = useSharedValue(0);
const isSeeking = useSharedValue(false);
const cacheProgress = useSharedValue(0);
const VolumeManager = Platform.isTV
? null
: require("react-native-volume-manager");
let getDownloadedItem = null;
if (!Platform.isTV) {
getDownloadedItem = downloadProvider.useDownload();
}
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
const lightHapticFeedback = useHaptic("light");
const setShowControls = useCallback((show: boolean) => {
_setShowControls(show);
lightHapticFeedback();
}, []);
const {
itemId,
audioIndex: audioIndexStr,
subtitleIndex: subtitleIndexStr,
mediaSourceId,
bitrateValue: bitrateValueStr,
offline: offlineStr,
} = useGlobalSearchParams<{
itemId: string;
audioIndex: string;
subtitleIndex: string;
mediaSourceId: string;
bitrateValue: string;
offline: string;
}>();
const [settings] = useSettings();
const insets = useSafeAreaInsets();
const offline = offlineStr === "true";
const audioIndex = audioIndexStr
? Number.parseInt(audioIndexStr, 10)
: undefined;
const subtitleIndex = subtitleIndexStr
? Number.parseInt(subtitleIndexStr, 10)
: -1;
const bitrateValue = bitrateValueStr
? Number.parseInt(bitrateValueStr, 10)
: BITRATES[0].value;
const [item, setItem] = useState<BaseItemDto | null>(null);
const [itemStatus, setItemStatus] = useState({
isLoading: true,
isError: false,
});
useEffect(() => {
const fetchItemData = async () => {
setItemStatus({ isLoading: true, isError: false });
try {
let fetchedItem: BaseItemDto | null = null;
if (offline && !Platform.isTV) {
const data = await getDownloadedItem.getDownloadedItem(itemId);
if (data) fetchedItem = data.item as BaseItemDto;
} else {
const res = await getUserLibraryApi(api!).getItem({
itemId,
userId: user?.Id,
});
fetchedItem = res.data;
}
setItem(fetchedItem);
setItemStatus({ isLoading: false, isError: false });
} catch (error) {
console.error("Failed to fetch item:", error);
setItemStatus({ isLoading: false, isError: true });
}
};
if (itemId) {
fetchItemData();
}
}, [itemId, offline, api, user?.Id]);
interface Stream {
mediaSource: MediaSourceInfo;
sessionId: string;
url: string;
}
const [stream, setStream] = useState<Stream | null>(null);
const [streamStatus, setStreamStatus] = useState({
isLoading: true,
isError: false,
});
useEffect(() => {
const fetchStreamData = async () => {
setStreamStatus({ isLoading: true, isError: false });
const native = await generateDeviceProfile();
try {
let result: Stream | null = null;
if (offline && !Platform.isTV) {
const data = await getDownloadedItem.getDownloadedItem(itemId);
if (!data?.mediaSource) return;
const url = await getDownloadedFileUrl(data.item.Id!);
if (item) {
result = { mediaSource: data.mediaSource, sessionId: "", url };
}
} else {
const res = await getStreamUrl({
api,
item,
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
userId: user?.Id,
audioStreamIndex: audioIndex,
maxStreamingBitrate: bitrateValue,
mediaSourceId: mediaSourceId,
subtitleStreamIndex: subtitleIndex,
deviceProfile: native,
});
if (!res) return;
const { mediaSource, sessionId, url } = res;
if (!sessionId || !mediaSource || !url) {
Alert.alert(
t("player.error"),
t("player.failed_to_get_stream_url"),
);
return;
}
result = { mediaSource, sessionId, url };
}
setStream(result);
setStreamStatus({ isLoading: false, isError: false });
} catch (error) {
console.error("Failed to fetch stream:", error);
setStreamStatus({ isLoading: false, isError: true });
}
};
fetchStreamData();
}, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
useEffect(() => {
if (!stream) return;
const reportPlaybackStart = async () => {
await getPlaystateApi(api!).reportPlaybackStart({
playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo,
});
};
reportPlaybackStart();
}, [stream]);
const togglePlay = async () => {
lightHapticFeedback();
setIsPlaying(!isPlaying);
if (isPlaying) {
await videoRef.current?.pause();
reportPlaybackProgress();
} else {
videoRef.current?.play();
await getPlaystateApi(api!).reportPlaybackStart({
playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo,
});
}
};
const reportPlaybackStopped = useCallback(async () => {
if (offline) return;
const currentTimeInTicks = msToTicks(progress.get());
await getPlaystateApi(api!).onPlaybackStopped({
itemId: item?.Id!,
mediaSourceId: mediaSourceId,
positionTicks: currentTimeInTicks,
playSessionId: stream?.sessionId!,
});
revalidateProgressCache();
}, [
api,
item,
mediaSourceId,
stream,
progress,
offline,
revalidateProgressCache,
]);
const stop = useCallback(() => {
reportPlaybackStopped();
setIsPlaybackStopped(true);
videoRef.current?.stop();
}, [videoRef, reportPlaybackStopped]);
useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
return () => {
beforeRemoveListener();
};
}, [navigation, stop]);
const currentPlayStateInfo = () => {
if (!stream) return;
return {
itemId: item?.Id!,
audioStreamIndex: audioIndex ? audioIndex : undefined,
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
mediaSourceId: mediaSourceId,
positionTicks: msToTicks(progress.get()),
isPaused: !isPlaying,
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
playSessionId: stream.sessionId,
isMuted: isMuted,
canSeek: true,
repeatMode: RepeatMode.RepeatNone,
playbackOrder: PlaybackOrder.Default,
};
};
const onProgress = useCallback(
async (data: ProgressUpdatePayload) => {
if (isSeeking.get() || isPlaybackStopped) return;
const { currentTime } = data.nativeEvent;
if (isBuffering) {
setIsBuffering(false);
}
progress.set(currentTime);
if (offline) return;
if (!item?.Id || !stream) return;
reportPlaybackProgress();
},
[
item?.Id,
audioIndex,
subtitleIndex,
mediaSourceId,
isPlaying,
stream,
isSeeking,
isPlaybackStopped,
isBuffering,
],
);
const onPipStarted = useCallback((e: PipStartedPayload) => {
const { pipStarted } = e.nativeEvent;
setIsPipStarted(pipStarted);
}, []);
const reportPlaybackProgress = useCallback(async () => {
if (!api || offline || !stream) return;
await getPlaystateApi(api).reportPlaybackProgress({
playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo,
});
}, [
api,
isPlaying,
offline,
stream,
item?.Id,
audioIndex,
subtitleIndex,
mediaSourceId,
progress,
]);
const startPosition = useMemo(() => {
if (offline) return 0;
return item?.UserData?.PlaybackPositionTicks
? ticksToSeconds(item.UserData.PlaybackPositionTicks)
: 0;
}, [item, offline]);
const volumeUpCb = useCallback(async () => {
if (Platform.isTV) return;
try {
const { volume: currentVolume } = await VolumeManager.getVolume();
const newVolume = Math.min(currentVolume + 0.1, 1.0);
await VolumeManager.setVolume(newVolume);
} catch (error) {
console.error("Error adjusting volume:", error);
}
}, []);
const [previousVolume, setPreviousVolume] = useState<number | null>(null);
const toggleMuteCb = useCallback(async () => {
if (Platform.isTV) return;
try {
const { volume: currentVolume } = await VolumeManager.getVolume();
const currentVolumePercent = currentVolume * 100;
if (currentVolumePercent > 0) {
// Currently not muted, so mute
setPreviousVolume(currentVolumePercent);
await VolumeManager.setVolume(0);
setIsMuted(true);
} else {
// Currently muted, so restore previous volume
const volumeToRestore = previousVolume || 50; // Default to 50% if no previous volume
await VolumeManager.setVolume(volumeToRestore / 100);
setPreviousVolume(null);
setIsMuted(false);
}
} catch (error) {
console.error("Error toggling mute:", error);
}
}, [previousVolume]);
const volumeDownCb = useCallback(async () => {
if (Platform.isTV) return;
try {
const { volume: currentVolume } = await VolumeManager.getVolume();
const newVolume = Math.max(currentVolume - 0.1, 0); // Decrease by 10%
console.log(
"Volume Down",
Math.round(currentVolume * 100),
"→",
Math.round(newVolume * 100),
);
await VolumeManager.setVolume(newVolume);
} catch (error) {
console.error("Error adjusting volume:", error);
}
}, []);
const setVolumeCb = useCallback(async (newVolume: number) => {
if (Platform.isTV) return;
try {
const clampedVolume = Math.max(0, Math.min(newVolume, 100));
console.log("Setting volume to", clampedVolume);
await VolumeManager.setVolume(clampedVolume / 100);
} catch (error) {
console.error("Error setting volume:", error);
}
}, []);
useWebSocket({
isPlaying: isPlaying,
togglePlay: togglePlay,
stopPlayback: stop,
offline,
toggleMute: toggleMuteCb,
volumeUp: volumeUpCb,
volumeDown: volumeDownCb,
setVolume: setVolumeCb,
});
const onPlaybackStateChanged = useCallback(
async (e: PlaybackStatePayload) => {
const { state, isBuffering, isPlaying } = e.nativeEvent;
if (state === "Playing") {
setIsPlaying(true);
reportPlaybackProgress();
if (!Platform.isTV) await activateKeepAwakeAsync();
return;
}
if (state === "Paused") {
setIsPlaying(false);
reportPlaybackProgress();
if (!Platform.isTV) await deactivateKeepAwake();
return;
}
if (isPlaying) {
setIsPlaying(true);
setIsBuffering(false);
} else if (isBuffering) {
setIsBuffering(true);
}
},
[reportPlaybackProgress],
);
const allAudio =
stream?.mediaSource.MediaStreams?.filter(
(audio) => audio.Type === "Audio",
) || [];
// Move all the external subtitles last, because vlc places them last.
const allSubs =
stream?.mediaSource.MediaStreams?.filter(
(sub) => sub.Type === "Subtitle",
).sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)) || [];
const externalSubtitles = allSubs
.filter((sub: any) => sub.DeliveryMethod === "External")
.map((sub: any) => ({
name: sub.DisplayTitle,
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
}));
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
const chosenSubtitleTrack = allSubs.find(
(sub) => sub.Index === subtitleIndex,
);
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
const initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
if (
chosenSubtitleTrack &&
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
) {
const finalIndex = notTranscoding
? allSubs.indexOf(chosenSubtitleTrack)
: textSubs.indexOf(chosenSubtitleTrack);
initOptions.push(`--sub-track=${finalIndex}`);
}
if (notTranscoding && chosenAudioTrack) {
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
}
const [isMounted, setIsMounted] = useState(false);
// Add useEffect to handle mounting
useEffect(() => {
setIsMounted(true);
return () => setIsMounted(false);
}, []);
if (itemStatus.isLoading || streamStatus.isLoading || !item || !stream) {
return (
<View className='w-screen h-screen flex flex-col items-center justify-center bg-black'>
<Loader />
</View>
);
}
if (itemStatus.isError || streamStatus.isError)
return (
<View className='w-screen h-screen flex flex-col items-center justify-center bg-black'>
<Text className='text-white'>{t("player.error")}</Text>
</View>
);
return (
<View style={{ flex: 1, backgroundColor: "black" }}>
<View
style={{
display: "flex",
width: "100%",
height: "100%",
position: "relative",
flexDirection: "column",
justifyContent: "center",
paddingLeft: ignoreSafeAreas ? 0 : insets.left,
paddingRight: ignoreSafeAreas ? 0 : insets.right,
}}
>
<VlcPlayerView
ref={videoRef}
source={{
uri: stream?.url || "",
autoplay: true,
isNetwork: true,
startPosition,
externalSubtitles,
initOptions,
}}
style={{ width: "100%", height: "100%" }}
onVideoProgress={onProgress}
progressUpdateInterval={1000}
onVideoStateChange={onPlaybackStateChanged}
onPipStarted={onPipStarted}
onVideoLoadEnd={() => {
setIsVideoLoaded(true);
}}
onVideoError={(e) => {
console.error("Video Error:", e.nativeEvent);
Alert.alert(
t("player.error"),
t("player.an_error_occured_while_playing_the_video"),
);
writeToLog("ERROR", "Video Error", e.nativeEvent);
}}
/>
</View>
{videoRef.current && !isPipStarted && isMounted === true ? (
<Controls
mediaSource={stream?.mediaSource}
item={item}
videoRef={videoRef}
togglePlay={togglePlay}
isPlaying={isPlaying}
isSeeking={isSeeking}
progress={progress}
cacheProgress={cacheProgress}
isBuffering={isBuffering}
showControls={showControls}
setShowControls={setShowControls}
setIgnoreSafeAreas={setIgnoreSafeAreas}
ignoreSafeAreas={ignoreSafeAreas}
isVideoLoaded={isVideoLoaded}
startPictureInPicture={videoRef?.current?.startPictureInPicture}
play={videoRef.current?.play}
pause={videoRef.current?.pause}
seek={videoRef.current?.seekTo}
enableTrickplay={true}
getAudioTracks={videoRef.current?.getAudioTracks}
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
offline={offline}
setSubtitleTrack={videoRef.current.setSubtitleTrack}
setSubtitleURL={videoRef.current.setSubtitleURL}
setAudioTrack={videoRef.current.setAudioTrack}
isVlc
/>
) : null}
</View>
);
}

View File

@@ -1,23 +0,0 @@
import { OfflineVideoPlayer } from "@/components/OfflineVideoPlayer";
import * as FileSystem from "expo-file-system";
import { useLocalSearchParams } from "expo-router";
import { useMemo } from "react";
import { View } from "react-native";
export default function page() {
const searchParams = useLocalSearchParams();
const { itemId, url } = searchParams as { itemId: string; url: string };
const fileUrl = useMemo(() => {
const u = FileSystem.documentDirectory + url;
return u;
}, [url]);
if (!fileUrl) return null;
return (
<View className="h-screen w-screen items-center justify-center">
{url && <OfflineVideoPlayer url={fileUrl} />}
</View>
);
}

View File

@@ -1,110 +0,0 @@
import { Text } from "@/components/common/Text";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { View } from "react-native";
const page: React.FC = () => {
const params = useLocalSearchParams();
const { id: seriesId, seasonIndex } = params as {
id: string;
seasonIndex: string;
};
useEffect(() => {
if (seriesId) {
console.log("seasonIndex", seasonIndex);
}
}, [seriesId]);
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: item } = useQuery({
queryKey: ["series", seriesId],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: seriesId,
}),
enabled: !!seriesId && !!api,
staleTime: 0,
});
const backdropUrl = useMemo(
() =>
getBackdropUrl({
api,
item,
quality: 90,
width: 1000,
}),
[item],
);
const logoUrl = useMemo(
() =>
getLogoImageUrlById({
api,
item,
}),
[item],
);
if (!item || !backdropUrl) return null;
return (
<ParallaxScrollView
headerImage={
<Image
source={{
uri: backdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
}
logo={
<>
{logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null}
</>
}
>
<View className="flex flex-col pt-4 pb-24">
<View className="px-4 py-4">
<Text className="text-3xl font-bold">{item?.Name}</Text>
<Text className="">{item?.Overview}</Text>
</View>
<View className="mb-4">
<NextUp seriesId={seriesId} />
</View>
<SeasonPicker item={item} />
</View>
</ParallaxScrollView>
);
};
export default page;

View File

@@ -1,87 +0,0 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/ListItem";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { clearLogs, readFromLog } from "@/utils/log";
import { useQuery } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
import { useAtom } from "jotai";
import { ScrollView, View } from "react-native";
import * as Haptics from "expo-haptics";
import { useFiles } from "@/hooks/useFiles";
export default function settings() {
const { logout } = useJellyfin();
const { deleteAllFiles } = useFiles();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: logs } = useQuery({
queryKey: ["logs"],
queryFn: async () => readFromLog(),
refetchInterval: 1000,
});
return (
<ScrollView>
<View className="p-4 flex flex-col gap-y-4 pb-12">
<Text className="font-bold text-2xl">Information</Text>
<View className="rounded-xl mb-4 overflow-hidden border-neutral-800 divide-y-2 divide-neutral-900">
<ListItem title="User" subTitle={user?.Name} />
<ListItem title="Server" subTitle={api?.basePath} />
</View>
<View className="flex flex-col space-y-2">
<Button color="black" onPress={logout}>
Log out
</Button>
<Button
color="red"
onPress={async () => {
await deleteAllFiles();
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success,
);
}}
>
Delete all downloaded files
</Button>
<Button
color="red"
onPress={async () => {
await clearLogs();
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success,
);
}}
>
Delete all logs
</Button>
</View>
<Text className="font-bold text-2xl">Logs</Text>
<View className="flex flex-col space-y-2">
{logs?.map((log, index) => (
<View key={index} className="bg-neutral-900 rounded-xl p-3">
<Text
className={`
mb-1
${log.level === "INFO" && "text-blue-500"}
${log.level === "ERROR" && "text-red-500"}
`}
>
{log.level}
</Text>
<Text className="text-xs">{log.message}</Text>
</View>
))}
{logs?.length === 0 && (
<Text className="opacity-50">No logs available</Text>
)}
</View>
</View>
</ScrollView>
);
}

View File

@@ -1,5 +1,5 @@
import { ScrollViewStyleReset } from "expo-router/html";
import { type PropsWithChildren } from "react";
import type { PropsWithChildren } from "react";
/**
* This file is web-only and used to configure the root HTML for every web page during static rendering.
@@ -7,13 +7,13 @@ import { type PropsWithChildren } from "react";
*/
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<html lang='en'>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charSet='utf-8' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
name='viewport'
content='width=device-width, initial-scale=1, shrink-to-fit=no'
/>
{/*

View File

@@ -9,9 +9,9 @@ export default function NotFoundScreen() {
<>
<Stack.Screen options={{ title: "Oops!" }} />
<ThemedView style={styles.container}>
<ThemedText type="title">This screen doesn't exist.</ThemedText>
<Link href={"/"} style={styles.link}>
<ThemedText type="link">Go to home screen!</ThemedText>
<ThemedText type='title'>This screen doesn't exist.</ThemedText>
<Link href={"/home"} style={styles.link}>
<ThemedText type='link'>Go to home screen!</ThemedText>
</Link>
</ThemedView>
</>

View File

@@ -1,128 +1,537 @@
import { JellyfinProvider } from "@/providers/JellyfinProvider";
import "@/augmentations";
import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider";
import {
JellyfinProvider,
apiAtom,
getOrSetDeviceId,
getTokenFromStorage,
} from "@/providers/JellyfinProvider";
import { JobQueueProvider } from "@/providers/JobQueueProvider";
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
import { WebSocketProvider } from "@/providers/WebSocketProvider";
import { type Settings, useSettings } from "@/utils/atoms/settings";
import {
BACKGROUND_FETCH_TASK,
BACKGROUND_FETCH_TASK_SESSIONS,
registerBackgroundFetchAsyncSessions,
} from "@/utils/background-tasks";
import {
LogProvider,
writeDebugLog,
writeErrorLog,
writeToLog,
} from "@/utils/log";
import { storage } from "@/utils/mmkv";
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { Platform } from "react-native";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useFonts } from "expo-font";
import * as NavigationBar from "expo-navigation-bar";
import { Stack, router } from "expo-router";
const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
import * as Device from "expo-device";
import * as FileSystem from "expo-file-system";
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { Stack, router, useSegments } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
import { getLocales } from "expo-localization";
import { Provider as JotaiProvider } from "jotai";
import { useEffect, useRef } from "react";
import { Platform, TouchableOpacity } from "react-native";
import { useEffect, useRef, useState } from "react";
import { I18nextProvider } from "react-i18next";
import { AppState, Appearance } from "react-native";
import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated";
import { userAtom } from "@/providers/JellyfinProvider";
import { store } from "@/utils/store";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import type { EventSubscription } from "expo-modules-core";
import type {
Notification,
NotificationResponse,
} from "expo-notifications/build/Notifications.types";
import type { ExpoPushToken } from "expo-notifications/build/Tokens.types";
import { useAtom } from "jotai";
import { Toaster } from "sonner-native";
import Feather from "@expo/vector-icons/Feather";
import { StatusBar } from "expo-status-bar";
import { Colors } from "@/constants/Colors";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import Video from "react-native-video";
import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar";
if (!Platform.isTV) {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
}
// Prevent the splash screen from auto-hiding before asset loading is complete.
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();
export const unstable_settings = {
initialRouteName: "(auth)/(tabs)/",
// Set the animation options. This is optional.
SplashScreen.setOptions({
duration: 500,
fade: true,
});
function useNotificationObserver() {
if (Platform.isTV) return;
useEffect(() => {
let isMounted = true;
function redirect(notification: typeof Notifications.Notification) {
const url = notification.request.content.data?.url;
if (url) {
router.push(url);
}
}
Notifications.getLastNotificationResponseAsync().then(
(response: { notification: any }) => {
if (!isMounted || !response?.notification) {
return;
}
redirect(response?.notification);
},
);
const subscription = Notifications.addNotificationResponseReceivedListener(
(response: { notification: any }) => {
redirect(response.notification);
},
);
return () => {
isMounted = false;
subscription.remove();
};
}, []);
}
if (!Platform.isTV) {
TaskManager.defineTask(BACKGROUND_FETCH_TASK_SESSIONS, async () => {
console.log("TaskManager ~ sessions trigger");
const api = store.get(apiAtom);
if (api === null || api === undefined) return;
const response = await getSessionApi(api).getSessions({
activeWithinSeconds: 360,
});
const result = response.data.filter((s) => s.NowPlayingItem);
Notifications.setBadgeCountAsync(result.length);
return BackgroundFetch.BackgroundFetchResult.NewData;
});
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
console.log("TaskManager ~ trigger");
const now = Date.now();
const settingsData = storage.getString("settings");
if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
const settings: Partial<Settings> = JSON.parse(settingsData);
const url = settings?.optimizedVersionsServerUrl;
if (!settings?.autoDownload || !url)
return BackgroundFetch.BackgroundFetchResult.NoData;
const token = getTokenFromStorage();
const deviceId = getOrSetDeviceId();
const baseDirectory = FileSystem.documentDirectory;
if (!token || !deviceId || !baseDirectory)
return BackgroundFetch.BackgroundFetchResult.NoData;
const jobs = await getAllJobsByDeviceId({
deviceId,
authHeader: token,
url,
});
console.log("TaskManager ~ Active jobs: ", jobs.length);
for (const job of jobs) {
if (job.status === "completed") {
const downloadUrl = `${url}download/${job.id}`;
const tasks = await BackGroundDownloader.checkForExistingDownloads();
if (tasks.find((task: { id: string }) => task.id === job.id)) {
console.log("TaskManager ~ Download already in progress: ", job.id);
continue;
}
BackGroundDownloader.download({
id: job.id,
url: downloadUrl,
destination: `${baseDirectory}${job.item.Id}.mp4`,
headers: {
Authorization: token,
},
})
.begin(() => {
console.log("TaskManager ~ Download started: ", job.id);
})
.done(() => {
console.log("TaskManager ~ Download completed: ", job.id);
saveDownloadedItemInfo(job.item);
BackGroundDownloader.completeHandler(job.id);
cancelJobById({
authHeader: token,
id: job.id,
url: url,
});
Notifications.scheduleNotificationAsync({
content: {
title: job.item.Name,
body: "Download completed",
data: {
url: "/downloads",
},
},
trigger: null,
});
})
.error((error: any) => {
console.log("TaskManager ~ Download error: ", job.id, error);
BackGroundDownloader.completeHandler(job.id);
Notifications.scheduleNotificationAsync({
content: {
title: job.item.Name,
body: "Download failed",
data: {
url: "/downloads",
},
},
trigger: null,
});
});
}
}
console.log(`Auto download started: ${new Date(now).toISOString()}`);
// Be sure to return the successful result type!
return BackgroundFetch.BackgroundFetchResult.NewData;
});
}
const checkAndRequestPermissions = async () => {
try {
const hasAskedBefore = storage.getString(
"hasAskedForNotificationPermission",
);
if (hasAskedBefore !== "true") {
const { status } = await Notifications.requestPermissionsAsync();
if (status === "granted") {
writeToLog("INFO", "Notification permissions granted.");
console.log("Notification permissions granted.");
} else {
writeToLog("ERROR", "Notification permissions denied.");
console.log("Notification permissions denied.");
}
storage.set("hasAskedForNotificationPermission", "true");
} else {
console.log("Already asked for notification permissions before.");
}
} catch (error) {
writeToLog(
"ERROR",
"Error checking/requesting notification permissions:",
error,
);
console.error("Error checking/requesting notification permissions:", error);
}
};
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
Appearance.setColorScheme("dark");
const queryClientRef = useRef<QueryClient>(
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60,
refetchOnMount: true,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
retryOnMount: true,
},
},
}),
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<JotaiProvider>
<ActionSheetProvider>
<I18nextProvider i18n={i18n}>
<Layout />
</I18nextProvider>
</ActionSheetProvider>
</JotaiProvider>
</GestureHandlerRootView>
);
}
const insets = useSafeAreaInsets();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0,
refetchOnMount: true,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
retryOnMount: true,
},
},
});
function Layout() {
const [settings] = useSettings();
const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom);
const appState = useRef(AppState.currentState);
const segments = useSegments();
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
i18n.changeLanguage(
settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en",
);
}, [settings?.preferedLanguage, i18n]);
if (!loaded) {
return null;
if (!Platform.isTV) {
useNotificationObserver();
const [expoPushToken, setExpoPushToken] = useState<ExpoPushToken>();
const notificationListener = useRef<EventSubscription>();
const responseListener = useRef<EventSubscription>();
useEffect(() => {
if (expoPushToken && api && user) {
api
?.post("/Streamyfin/device", {
token: expoPushToken.data,
deviceId: getOrSetDeviceId(),
userId: user.Id,
})
.then((_) => console.log("Posted expo push token"))
.catch((_) =>
writeErrorLog("Failed to push expo push token to plugin"),
);
} else console.log("No token available");
}, [api, expoPushToken, user]);
async function registerNotifications() {
if (Platform.OS === "android") {
console.log("Setting android notification channel 'default'");
await Notifications?.setNotificationChannelAsync("default", {
name: "default",
});
}
await checkAndRequestPermissions();
if (!Platform.isTV && user && user.Policy?.IsAdministrator) {
await registerBackgroundFetchAsyncSessions();
}
// only create push token for real devices (pointless for emulators)
if (Device.isDevice) {
Notifications?.getExpoPushTokenAsync()
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
.catch((reason: any) => console.log("Failed to get token", reason));
}
}
useEffect(() => {
registerNotifications();
notificationListener.current =
Notifications?.addNotificationReceivedListener(
(notification: Notification) => {
console.log(
"Notification received while app running",
notification,
);
},
);
responseListener.current =
Notifications?.addNotificationResponseReceivedListener(
(response: NotificationResponse) => {
// Currently the notifications supported by the plugin will send data for deep links.
const { title, data } = response.notification.request.content;
writeDebugLog(
`Notification ${title} opened`,
response.notification.request.content,
);
if (data && Object.keys(data).length > 0) {
const type = data?.type?.toLower?.();
const itemId = data?.id;
switch (type) {
case "movie":
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
break;
case "episode":
// We just clicked a notification for an individual episode.
if (itemId) {
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
}
// summarized season notification for multiple episodes. Bring them to series season
else {
const seriesId = data.seriesId;
const seasonIndex = data.seasonIndex;
if (seasonIndex) {
router.push(
`/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`,
);
} else {
router.push(`/(auth)/(tabs)/home/series/${seriesId}`);
}
}
break;
}
}
},
);
return () => {
notificationListener.current &&
Notifications?.removeNotificationSubscription(
notificationListener.current,
);
responseListener.current &&
Notifications?.removeNotificationSubscription(
responseListener.current,
);
};
}, []);
useEffect(() => {
if (Platform.isTV) return;
if (segments.includes("direct-player" as never)) {
return;
}
// If the user has auto rotate enabled, unlock the orientation
if (settings.followDeviceOrientation === true) {
ScreenOrientation.unlockAsync();
} else {
// If the user has auto rotate disabled, lock the orientation to portrait
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP,
);
}
}, [settings.followDeviceOrientation, segments]);
useEffect(() => {
const subscription = AppState.addEventListener(
"change",
(nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
BackGroundDownloader.checkForExistingDownloads();
}
},
);
BackGroundDownloader.checkForExistingDownloads();
return () => {
subscription.remove();
};
}, []);
}
return (
<QueryClientProvider client={queryClientRef.current}>
<JotaiProvider>
<QueryClientProvider client={queryClient}>
<JobQueueProvider>
<JellyfinProvider>
<StatusBar style="light" backgroundColor="#000" />
<ThemeProvider value={DarkTheme}>
<Stack screenOptions={{}}>
<Stack.Screen
name="(auth)/(tabs)"
options={{
headerShown: false,
title: "Home",
}}
/>
<Stack.Screen
name="(auth)/settings"
options={{
headerShown: true,
title: "Settings",
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="(auth)/downloads"
options={{
headerShown: true,
title: "Downloads",
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="(auth)/items/[id]/page"
options={{
title: "",
headerShown: false,
}}
/>
<Stack.Screen
name="(auth)/collections/[collection]/page"
options={{
title: "",
headerShown: true,
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="(auth)/series/[id]/page"
options={{
title: "",
headerShown: false,
}}
/>
<Stack.Screen
name="login"
options={{ headerShown: false, title: "Login" }}
/>
<Stack.Screen name="+not-found" />
</Stack>
<CurrentlyPlayingBar />
</ThemeProvider>
<PlaySettingsProvider>
<LogProvider>
<WebSocketProvider>
<DownloadProvider>
<BottomSheetModalProvider>
<SystemBars style='light' hidden={false} />
<ThemeProvider value={DarkTheme}>
<Stack initialRouteName='(auth)/(tabs)'>
<Stack.Screen
name='(auth)/(tabs)'
options={{
headerShown: false,
title: "",
header: () => null,
}}
/>
<Stack.Screen
name='(auth)/player'
options={{
headerShown: false,
title: "",
header: () => null,
}}
/>
<Stack.Screen
name='login'
options={{
headerShown: true,
title: "",
headerTransparent: true,
}}
/>
<Stack.Screen name='+not-found' />
</Stack>
<Toaster
duration={4000}
toastOptions={{
style: {
backgroundColor: "#262626",
borderColor: "#363639",
borderWidth: 1,
},
titleStyle: {
color: "white",
},
}}
closeButton
/>
</ThemeProvider>
</BottomSheetModalProvider>
</DownloadProvider>
</WebSocketProvider>
</LogProvider>
</PlaySettingsProvider>
</JellyfinProvider>
</JotaiProvider>
</JobQueueProvider>
</QueryClientProvider>
);
}
function saveDownloadedItemInfo(item: BaseItemDto) {
try {
const downloadedItems = storage.getString("downloadedItems");
const items: BaseItemDto[] = downloadedItems
? JSON.parse(downloadedItems)
: [];
const existingItemIndex = items.findIndex((i) => i.Id === item.Id);
if (existingItemIndex !== -1) {
items[existingItemIndex] = item;
} else {
items.push(item);
}
storage.set("downloadedItems", JSON.stringify(items));
} catch (error) {
writeToLog("ERROR", "Failed to save downloaded item information:", error);
console.error("Failed to save downloaded item information:", error);
}
}

View File

@@ -1,34 +1,101 @@
import { Button } from "@/components/Button";
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
import { PreviousServersList } from "@/components/PreviousServersList";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { useAtom } from "jotai";
import React, { useMemo, useState } from "react";
import { KeyboardAvoidingView, Platform, View } from "react-native";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtomValue } from "jotai";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import {
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
TouchableOpacity,
View,
} from "react-native";
import { Keyboard } from "react-native";
import { t } from "i18next";
import { z } from "zod";
const CredentialsSchema = z.object({
username: z.string().min(1, "Username is required"),
username: z.string().min(1, t("login.username_required")),
});
const Login: React.FC = () => {
const { setServer, login, removeServer } = useJellyfin();
const [api] = useAtom(apiAtom);
const api = useAtomValue(apiAtom);
const navigation = useNavigation();
const params = useLocalSearchParams();
const { setServer, login, removeServer, initiateQuickConnect } =
useJellyfin();
const [serverURL, setServerURL] = useState<string>("");
const {
apiUrl: _apiUrl,
username: _username,
password: _password,
} = params as { apiUrl: string; username: string; password: string };
const [loadingServerCheck, setLoadingServerCheck] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [serverURL, setServerURL] = useState<string>(_apiUrl);
const [serverName, setServerName] = useState<string>("");
const [credentials, setCredentials] = useState<{
username: string;
password: string;
}>({
username: "",
password: "",
username: _username,
password: _password,
});
const [loading, setLoading] = useState<boolean>(false);
/**
* A way to auto login based on a link
*/
useEffect(() => {
(async () => {
if (_apiUrl) {
await setServer({
address: _apiUrl,
});
setTimeout(() => {
if (_username && _password) {
setCredentials({ username: _username, password: _password });
login(_username, _password);
}
}, 300);
}
})();
}, [_apiUrl, _username, _password]);
useEffect(() => {
navigation.setOptions({
headerTitle: serverName,
headerLeft: () =>
api?.basePath ? (
<TouchableOpacity
onPress={() => {
removeServer();
}}
className='flex flex-row items-center'
>
<Ionicons name='chevron-back' size={18} color={Colors.primary} />
<Text className='ml-2 text-purple-600'>
{t("login.change_server")}
</Text>
</TouchableOpacity>
) : null,
});
}, [serverName, navigation, api?.basePath]);
const handleLogin = async () => {
Keyboard.dismiss();
setLoading(true);
try {
const result = CredentialsSchema.safeParse(credentials);
@@ -36,128 +103,246 @@ const Login: React.FC = () => {
await login(credentials.username, credentials.password);
}
} catch (error) {
console.error(error);
if (error instanceof Error) {
Alert.alert(t("login.connection_failed"), error.message);
} else {
Alert.alert(
t("login.connection_failed"),
t("login.an_unexpected_error_occured"),
);
}
} finally {
setLoading(false);
}
};
const parsedServerURL = useMemo(() => {
let parsedServerURL = serverURL.trim();
/**
* Checks the availability and validity of a Jellyfin server URL.
*
* This function attempts to connect to a Jellyfin server using the provided URL.
* It tries both HTTPS and HTTP protocols, with a timeout to handle long 404 responses.
*
* @param {string} url - The base URL of the Jellyfin server to check.
* @returns {Promise<string | undefined>} A Promise that resolves to:
* - The full URL (including protocol) if a valid Jellyfin server is found.
* - undefined if no valid server is found at the given URL.
*
* Side effects:
* - Sets loadingServerCheck state to true at the beginning and false at the end.
* - Logs errors and timeout information to the console.
*/
const checkUrl = useCallback(async (url: string) => {
setLoadingServerCheck(true);
if (parsedServerURL) {
parsedServerURL = parsedServerURL.endsWith("/")
? parsedServerURL.replace("/", "")
: parsedServerURL;
parsedServerURL = parsedServerURL.startsWith("http")
? parsedServerURL
: "http://" + parsedServerURL;
try {
const response = await fetch(`${url}/System/Info/Public`, {
mode: "cors",
});
return parsedServerURL;
if (response.ok) {
const data = (await response.json()) as PublicSystemInfo;
setServerName(data.ServerName || "");
return url;
}
return undefined;
} catch {
return undefined;
} finally {
setLoadingServerCheck(false);
}
}, []);
/**
* Handles the connection attempt to a Jellyfin server.
*
* This function trims the input URL, checks its validity using the `checkUrl` function,
* and sets the server address if a valid connection is established.
*
* @param {string} url - The URL of the Jellyfin server to connect to.
*
* @returns {Promise<void>}
*
* Side effects:
* - Calls `checkUrl` to validate the server URL.
* - Shows an alert if the connection fails.
* - Sets the server address using `setServer` if the connection is successful.
*
*/
const handleConnect = useCallback(async (url: string) => {
url = url.trim().replace(/\/$/, "");
const result = await checkUrl(url);
if (result === undefined) {
Alert.alert(
t("login.connection_failed"),
t("login.could_not_connect_to_server"),
);
return;
}
return "";
}, [serverURL]);
await setServer({ address: url });
}, []);
const handleConnect = (url: string) => {
setServer({ address: url });
const handleQuickConnect = async () => {
try {
const code = await initiateQuickConnect();
if (code) {
Alert.alert(
t("login.quick_connect"),
t("login.enter_code_to_login", { code: code }),
[
{
text: t("login.got_it"),
},
],
);
}
} catch (error) {
Alert.alert(
t("login.error_title"),
t("login.failed_to_initiate_quick_connect"),
);
}
};
if (api?.basePath) {
return (
return (
<SafeAreaView style={{ flex: 1, paddingBottom: 16 }}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View className="flex flex-col px-4 justify-center h-full gap-y-2">
<View>
<Text className="text-3xl font-bold">Jellyfin</Text>
<Text className="opacity-50 mb-2">Server: {api.basePath}</Text>
<Button
color="black"
onPress={() => {
removeServer();
setServerURL("");
}}
justify="between"
iconLeft={
<Ionicons name="arrow-back-outline" size={18} color={"white"} />
}
>
Change server
</Button>
</View>
<View className="flex flex-col space-y-2">
<Text className="text-2xl font-bold">Log in</Text>
<Input
placeholder="Username"
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
autoFocus
secureTextEntry={false}
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="username"
clearButtonMode="while-editing"
maxLength={500}
/>
{api?.basePath ? (
<>
<View className='flex flex-col h-full relative items-center justify-center'>
<View className='px-4 -mt-20 w-full'>
<View className='flex flex-col space-y-2'>
<Text className='text-2xl font-bold -mb-2'>
{serverName ? (
<>
{`${t("login.login_to_title")} `}
<Text className='text-purple-600'>{serverName}</Text>
</>
) : (
t("login.login_title")
)}
</Text>
<Text className='text-xs text-neutral-400'>
{api.basePath}
</Text>
<Input
placeholder={t("login.username_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
keyboardType='default'
returnKeyType='done'
autoCapitalize='none'
// Changed from username to oneTimeCode because it is a known issue in RN
// https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037
textContentType='oneTimeCode'
clearButtonMode='while-editing'
maxLength={500}
/>
<Input
className="mb-2"
placeholder="Password"
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
value={credentials.password}
secureTextEntry
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="password"
clearButtonMode="while-editing"
maxLength={500}
/>
</View>
<Input
placeholder={t("login.password_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
value={credentials.password}
secureTextEntry
keyboardType='default'
returnKeyType='done'
autoCapitalize='none'
textContentType='password'
clearButtonMode='while-editing'
maxLength={500}
/>
<View className='flex flex-row items-center justify-between'>
<Button
onPress={handleLogin}
loading={loading}
className='flex-1 mr-2'
>
{t("login.login_button")}
</Button>
<TouchableOpacity
onPress={handleQuickConnect}
className='p-2 bg-neutral-900 rounded-xl h-12 w-12 flex items-center justify-center'
>
<MaterialCommunityIcons
name='cellphone-lock'
size={24}
color='white'
/>
</TouchableOpacity>
</View>
</View>
</View>
<Button onPress={handleLogin} loading={loading}>
Log in
</Button>
</View>
<View className='absolute bottom-0 left-0 w-full px-4 mb-2' />
</View>
</>
) : (
<>
<View className='flex flex-col h-full items-center justify-center w-full'>
<View className='flex flex-col gap-y-2 px-4 w-full -mt-36'>
<Image
style={{
width: 100,
height: 100,
marginLeft: -23,
marginBottom: -20,
}}
source={require("@/assets/images/StreamyFinFinal.png")}
/>
<Text className='text-3xl font-bold'>Streamyfin</Text>
<Text className='text-neutral-500'>
{t("server.enter_url_to_jellyfin_server")}
</Text>
<Input
aria-label='Server URL'
placeholder={t("server.server_url_placeholder")}
onChangeText={setServerURL}
value={serverURL}
keyboardType='url'
returnKeyType='done'
autoCapitalize='none'
textContentType='URL'
maxLength={500}
/>
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => {
await handleConnect(serverURL);
}}
className='w-full grow'
>
{t("server.connect_button")}
</Button>
<JellyfinServerDiscovery
onServerSelect={async (server) => {
setServerURL(server.address);
if (server.serverName) {
setServerName(server.serverName);
}
await handleConnect(server.address);
}}
/>
<PreviousServersList
onServerSelect={async (s) => {
await handleConnect(s.address);
}}
/>
</View>
</View>
</>
)}
</KeyboardAvoidingView>
);
}
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View className="flex flex-col px-4 justify-center h-full">
<View className="flex flex-col gap-y-2">
<Text className="text-3xl font-bold">Jellyfin</Text>
<Text className="opacity-50">Enter a server adress</Text>
<Input
className="mb-2"
placeholder="http(s)://..."
onChangeText={setServerURL}
value={serverURL}
autoFocus
secureTextEntry={false}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
maxLength={500}
/>
<Button onPress={() => handleConnect(parsedServerURL)}>
Connect
</Button>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,40 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_DE_RGB_blk_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M39.3926,14.69775H35.67092V8.731h.92676V13.8457H39.3926Z" style="fill: #fff"/>
<path d="M40.32912,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,40.32912,13.42432Zm2.89453-.38477v-.37646L42.124,12.7334c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,43.22365,13.03955Z" style="fill: #fff"/>
<path d="M45.27639,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074H48.6299v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C46,14.772,45.27639,13.87061,45.27639,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C46.64943,10.91846,46.19436,11.49707,46.19436,12.44434Z" style="fill: #fff"/>
<path d="M54.74709,13.48193a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422A2.07685,2.07685,0,0,1,52.792,10.10791c1.25293,0,2.00879.856,2.00879,2.27V12.688H51.62111v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,51.62111,12.03076Z" style="fill: #fff"/>
<path d="M55.99416,10.19482h.85547v.71533H56.916a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M63.51955,8.86328a.57572.57572,0,1,1,.5752.5415A.54735.54735,0,0,1,63.51955,8.86328Zm.13281,1.33154h.88477v4.50293h-.88477Z" style="fill: #fff"/>
<path d="M65.97121,10.19482h.85547v.72363h.06641a1.36385,1.36385,0,0,1,2.49316,0h.07031a1.46325,1.46325,0,0,1,1.36914-.81055,1.33821,1.33821,0,0,1,1.43848,1.48828v3.10156h-.88867V11.82813c0-.60791-.29-.90576-.873-.90576a.91167.91167,0,0,0-.9502.94287v2.83252h-.873V11.74121a.78468.78468,0,0,0-.86816-.81885.96854.96854,0,0,0-.95117,1.02148v2.75391h-.88867Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
assets/icons/heart.fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icons/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/house.fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
assets/images/icon-mono.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -0,0 +1,65 @@
<svg
type="certified"
viewBox="0 0 80 80"
preserveAspectRatio="xMidYMid"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g transform="translate(2.29, 0)">
<path
d="M42.1942857,18.8022857 C44.3794286,18.608 49.1565714,18.7177143 51.4902857,21.0057143 C51.6297143,21.1451429 51.5085714,21.4605714 51.3097143,21.408 C47.8902857,20.4868571 42.5577143,25.0217143 39.1017143,22.0891429 C39.008,22.9485714 38.2331429,27.0857143 32.3314286,26.4731429 C32.192,26.4594286 32.1371429,26.304 32.24,26.2171429 C33.1542857,25.44 34.2765714,23.2891429 33.3142857,21.9154286 C30.3108571,23.9085714 28.7565714,23.9954286 23.2182857,21.5954286 C23.0377143,21.5177143 23.1451429,21.2228571 23.3577143,21.1748571 C24.5074286,20.9165714 27.2434286,19.9222857 29.696,19.4582857 C30.1645714,19.3691429 30.624,19.3165714 31.0674286,19.312 C28.528,18.7062857 27.4217143,18.1805714 25.7485714,18.1874286 C25.5657143,18.1897143 25.4742857,17.9611429 25.6068571,17.8354286 C28.224,15.3188571 32.9691429,15.1885714 35.2548571,17.0628571 L33.2068571,12.7862857 L35.696,12.4114286 C35.696,12.4114286 36.3451429,14.6925714 36.9257143,16.7428571 C39.5177143,13.904 43.5268571,14.192 44.8777143,16.672 C44.9577143,16.8182857 44.8251429,16.992 44.6605714,16.9622857 C43.3005714,16.7314286 42.3702857,17.8628571 42.1737143,18.7977143 L42.1942857,18.8022857"
id="Fill-2"
fill="#00912D"
></path>
<mask id="mask-2" fill="white">
<polygon
points="0.137142857 0.921142857 75.0534777 0.921142857 75.0534777 79.8628571 0.137142857 79.8628571"
></polygon>
</mask>
<path
d="M13.0491429,59.1817143 C9.90628571,55.3554286 7.86971429,50.576 7.51771429,44.9622857 C6.912,35.2342857 10.2354286,26.0845714 23.1794286,21.4834286 C23.1908571,21.5245714 23.1725714,21.5748571 23.2182857,21.5954286 C23.0377143,21.5177143 23.1451429,21.2228571 23.3577143,21.1748571 C24.5074286,20.9165714 27.2434286,19.92 29.696,19.4582857 C30.1645714,19.3691429 30.624,19.3165714 31.0674286,19.3097143 C28.528,18.7062857 27.4217143,18.1805714 25.7485714,18.1874286 C25.5657143,18.1897143 25.4742857,17.9611429 25.6068571,17.8331429 C28.224,15.3165714 32.9691429,15.1885714 35.2548571,17.0628571 L33.2068571,12.784 L35.696,12.4114286 C35.696,12.4114286 36.3451429,14.6902857 36.9257143,16.7428571 C39.5177143,13.904 43.5268571,14.192 44.8777143,16.672 C44.9577143,16.8182857 44.8251429,16.992 44.6605714,16.9622857 C43.3005714,16.7314286 42.3702857,17.8628571 42.1737143,18.7977143 L42.1942857,18.8022857 C44.3794286,18.608 49.1565714,18.7177143 51.4902857,21.0057143 C51.328,20.8502857 51.1337143,20.7245714 50.9508571,20.5874286 C60.2765714,23.504 66.7474286,30.1531429 67.44,41.2251429 C67.8811429,48.2948571 65.5702857,54.3885714 61.568,59.1154286 C62.784,59.2891429 63.9931429,59.4925714 65.2045714,59.6937143 C70.304,53.4537143 73.2502857,45.5428571 73.2502857,37.056 C73.2502857,17.7165714 57.5337143,2.56685714 37.472,2.56685714 C17.4102857,2.56685714 1.69371429,17.7165714 1.69371429,37.056 C1.69371429,45.5565714 4.64,53.472 9.744,59.7097143 C10.8434286,59.5268571 11.9451429,59.3462857 13.0491429,59.1817143"
fill="#FFD700"
mask="url(#mask-2)"
></path>
<path
d="M9.744,59.7097143 C4.64,53.472 1.69371429,45.5565714 1.69371429,37.056 C1.69371429,17.7165714 17.4102857,2.56685714 37.472,2.56685714 C57.5337143,2.56685714 73.2502857,17.7165714 73.2502857,37.056 C73.2502857,45.5428571 70.304,53.4537143 65.2045714,59.6937143 C65.8125714,59.7942857 66.4205714,59.8742857 67.0285714,59.984 C71.9497143,53.6457143 74.8937143,45.6982857 74.8937143,37.056 C74.8937143,16.3862857 58.1394286,0.921142857 37.472,0.921142857 C16.8022857,0.921142857 0.048,16.3862857 0.048,37.056 C0.048,45.7074286 2.99885714,53.6594286 7.92914286,59.9977143 C8.53257143,59.8902857 9.13828571,59.8102857 9.744,59.7097143"
fill="#FA6E0F"
mask="url(#mask-2)"
></path>
<path
d="M58.2857143,74.9394286 C62.3748571,75.1954286 65.7874286,77.2137143 67.8468571,79.9474286 C67.9131429,80.0182857 68.0114286,80.016 68.0411429,79.9382857 C68.7451429,77.0971429 68.9394286,74.0662857 68.5851429,71.0125714 C68.5874286,70.9805714 68.6125714,70.9577143 68.6537143,70.9485714 C70.576,70.3428571 72.7017143,70.0137143 74.9645714,70.0457143 C75.0857143,70.0594286 75.0834286,69.9405714 74.9554286,69.8194286 C72.5577143,67.4994286 69.6297143,65.6914286 66.416,64.5417143 C65.3051429,67.68 64.2217143,70.816 63.1565714,73.9634286 C63.136,74.0228571 63.0514286,74.0594286 62.9645714,74.0434286 L58.2857143,74.9394286"
fill="#0AC855"
mask="url(#mask-2)"
></path>
<path
d="M62.9645714,74.0434286 L58.2857143,74.9394286 C58.2857143,74.9394286 58.3451429,74.512 58.528,73.3325714 C60.9417143,73.6754286 62.9645714,74.0434286 62.9645714,74.0434286"
fill="#0B4902"
></path>
<g transform="translate(0, 20.57)">
<mask id="mask-4" fill="white">
<polygon
points="0.137142857 0.016 67.4935952 0.016 67.4935952 59.2914286 0.137142857 59.2914286"
></polygon>
</mask>
<path
d="M13.0765714,38.6057143 C29.1177143,36.2605714 45.5222857,36.2354286 61.568,38.544 C65.5702857,33.8171429 67.8811429,27.7234286 67.44,20.6537143 C66.7474286,9.58171429 60.2765714,2.93257143 50.9508571,0.016 C51.1337143,0.153142857 51.328,0.278857143 51.4902857,0.434285714 C51.6297143,0.573714286 51.5085714,0.889142857 51.3097143,0.836571429 C47.8902857,-0.0845714286 42.5577143,4.45028571 39.1017143,1.51771429 C39.008,2.37485714 38.2331429,6.51428571 32.3314286,5.90171429 C32.192,5.888 32.1371429,5.73257143 32.24,5.64571429 C33.1542857,4.86857143 34.2765714,2.71542857 33.3142857,1.344 C30.3108571,3.33714286 28.7565714,3.424 23.2182857,1.024 C23.1725714,1.00342857 23.1908571,0.953142857 23.1794286,0.912 C10.2354286,5.51314286 6.912,14.6628571 7.51771429,24.3908571 C7.86971429,30.0091429 9.93142857,34.7748571 13.0765714,38.6057143"
fill="#FA3200"
mask="url(#mask-4)"
></path>
<path
d="M12.0868571,53.472 C12,53.488 11.9154286,53.4514286 11.8948571,53.392 C10.8274286,50.2445714 9.73485714,47.0971429 8.62171429,43.9611429 C5.41028571,45.1108571 2.49371429,46.9302857 0.0982857143,49.248 C-0.0297142857,49.3691429 -0.032,49.488 0.0891428571,49.4742857 C2.352,49.4422857 4.47771429,49.7714286 6.4,50.3771429 C6.44114286,50.3862857 6.46628571,50.4091429 6.46857143,50.4411429 C6.11428571,53.4948571 6.30857143,56.5257143 7.01257143,59.3668571 C7.04228571,59.4445714 7.14057143,59.4468571 7.20685714,59.376 C9.26628571,56.6422857 12.6742857,54.624 16.7657143,54.368 L12.0868571,53.472"
fill="#0AC855"
mask="url(#mask-4)"
></path>
</g>
<path
d="M62.9645714,74.0434286 C46.192,71.104 28.8571429,71.104 12.0868571,74.0434286 C12,74.0594286 11.9154286,74.0228571 11.8948571,73.9634286 C10.3428571,69.3851429 8.74285714,64.8182857 7.09257143,60.2628571 C7.06971429,60.1988571 7.14057143,60.1257143 7.248,60.1074286 C27.1885714,56.464 47.8605714,56.464 67.8034286,60.1074286 C67.9108571,60.1257143 67.9817143,60.1988571 67.9565714,60.2628571 C66.3085714,64.8182857 64.7085714,69.3851429 63.1565714,73.9634286 C63.136,74.0228571 63.0514286,74.0594286 62.9645714,74.0434286"
fill="#00912D"
></path>
<path
d="M12.0868571,74.0434286 L16.7657143,74.9394286 C16.7657143,74.9394286 16.704,74.512 16.5211429,73.3325714 C14.1074286,73.6754286 12.0868571,74.0434286 12.0868571,74.0434286"
fill="#0B4902"
></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 49 KiB

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