Quick Connect optimization

This commit is contained in:
Violet Caulfield
2025-10-29 00:39:10 -05:00
parent 314a67b03d
commit ea2b784137
9 changed files with 280 additions and 652 deletions

BIN
assets/icons/teal-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -1,407 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="300mm"
height="300mm"
viewBox="0 0 300 300"
version="1.1"
id="svg1"
xml:space="preserve"
sodipodi:docname="JellifyFinalGradientTealPurpleFinal.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.70710678"
inkscape:cx="444.06306"
inkscape:cy="514.06663"
inkscape:window-width="2560"
inkscape:window-height="1361"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg1"
showgrid="false" /><defs
id="defs1"><linearGradient
id="linearGradient1"
inkscape:collect="always"><stop
style="stop-color:#00dbb9;stop-opacity:1;"
offset="0"
id="stop2" /><stop
style="stop-color:#7317ff;stop-opacity:1;"
offset="1"
id="stop1" /></linearGradient><inkscape:path-effect
effect="mirror_symmetry"
start_point="36.829795,103.65226"
end_point="163.35938,103.65226"
center_point="100.09459,103.65226"
id="path-effect27"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="Y"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="true"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect26"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect25"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="zerowidth"
offset_points="1,2.5"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect24"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect23"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="butt"
end_linecap_type="zerowidth"
offset_points="0.28721364,1.9659181 | 1.0065447,1.3281153 | 1.7656209,0.14729167"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect22"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="round"
offset_points="0.021811409,4.5200294 | 2.9894369,3.0532151 | 4.0214422,4.3410551"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect21"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="round"
offset_points="0,3.0010829 | 1.5006767,3.0586652 | 2.7911139,4.8447845"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect20"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="round"
offset_points="0.065755614,3.9759006 | 2.5006233,4.1433183 | 4.7821816,4.2678689"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect19"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="round"
offset_points="0.046321001,3.1112262 | 1.5105021,3.3915097 | 3.5871741,4.0865502"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect18"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="zerowidth"
offset_points="0.2,2.5 | 1,2.5 | 1.8,2.5"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect17"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="zerowidth"
offset_points="0.69542096,1.5137821 | 1.999296,4.5220552 | 3.574709,1.3221339"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect16"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="zerowidth"
offset_points="0.2,2.5 | 1.0010114,5.1044674 | 1.8,2.5"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="powerstroke"
message=""
id="path-effect14"
is_visible="true"
lpeversion="1.3"
scale_width="1"
interpolator_type="CentripetalCatmullRom"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
end_linecap_type="butt"
offset_points="0.34651289,2.842816 | 1,5.8779234 | 0.20052903,2.327192"
linejoin_type="round"
miter_limit="4"
not_jump="false"
sort_points="true" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect8"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect7"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="14.80014,150"
end_point="276.48117,150"
center_point="145.64065,150"
id="path-effect4"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect1"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="spiro"
id="path-effect15"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect13"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect12"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect11"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="spiro"
id="path-effect10"
is_visible="true"
lpeversion="1" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect9"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="14.80014,150"
end_point="276.48117,150"
center_point="145.64065,150"
id="path-effect5"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect3"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect2"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><inkscape:path-effect
effect="mirror_symmetry"
start_point="0,150"
end_point="300,150"
center_point="150,150"
id="path-effect6"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="horizontal"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="76.596825"
y1="82.716858"
x2="233.49379"
y2="239.61383"
gradientUnits="userSpaceOnUse" /></defs><path
id="path6"
style="display:inline;fill:url(#linearGradient2);fill-rule:nonzero;stroke:none;stroke-width:5.66499;stroke-linecap:round"
d="M 139.3269,45.768783 C 126.88229,45.706148 95.328135,48.241638 71.292289,73.203284 61.843322,83.016179 47.31339,102.04964 44.663424,128.3317 c -6.621328,65.66952 47.193245,92.9287 65.103006,70.99929 -32.613971,12.4543 -59.632157,-25.84645 -53.371939,-68.86298 3.10699,-21.34941 15.9332,-37.938267 25.19071,-47.436902 5.427134,-5.568491 22.887769,-21.542941 47.604329,-25.073922 35.98193,-5.140346 81.37009,11.685423 69.50325,52.880494 21.40391,-17.159778 -0.14812,-64.770831 -59.36588,-65.068897 z m 30.06896,24.740092 c -0.13986,0.780925 15.7155,4.18369 18.12809,20.062341 1.84384,12.005594 -1.95562,24.498424 -4.14649,30.378504 -4.3814,11.75925 -12.17947,24.61767 -24.35033,36.75954 -12.17086,12.14187 -24.85679,20.30474 -37.63186,24.94269 -5.86887,2.13066 -18.24679,5.7681 -30.193502,3.83129 -15.558836,-2.49433 -18.996319,-17.81412 -20.177063,-17.60306 -1.194435,0.21437 1.39647,18.53479 18.936312,23.24974 13.319633,3.59786 27.519723,0.16754 33.996373,-1.85311 3.23912,-1.01058 6.47122,-2.23697 9.679,-3.67781 2.93812,3.21771 11.67161,9.65724 25.83356,3.22667 0.51974,2.27797 1.50261,5.26982 2.30529,8.72608 1.72873,10.67723 2.13014,19.22365 8.92193,28.33523 4.94229,5.39527 8.49946,11.66414 11.60748,18.21932 3.07739,6.49078 4.05348,16.71238 0.013,25.20955 -2.48575,5.22727 5.35512,8.95572 7.84087,3.72845 5.34005,-11.23027 4.62575,-23.9551 -0.44855,-33.54007 -2.8174,-7.44396 -7.35856,-11.57584 -11.88507,-20.45973 -5.93146,-7.95448 -4.20766,-14.06077 -4.92476,-23.05027 -0.40158,-5.97052 -1.96142,-10.8536 -3.63182,-13.19506 0.17381,-0.13365 0.34794,-0.26943 0.52297,-0.4067 1.73545,2.52942 4.90398,5.86046 8.0455,9.26094 6.76431,7.51395 13.01667,15.63107 16.51527,25.21034 1.35773,4.14855 2.10119,8.60352 3.45664,13.13305 0.98929,3.30466 3.27759,9.40579 7.58661,13.54595 6.38008,6.1429 15.91935,7.80933 24.11895,4.23385 4.93753,-2.20003 1.70548,-9.61259 -3.26647,-7.49154 -4.66716,2.03515 -10.78159,1.38248 -15.14998,-2.72076 -2.93588,-2.76759 -4.86938,-7.35322 -5.80326,-9.99526 -1.47415,-4.17243 -2.52565,-8.67391 -4.25814,-13.11651 -3.34012,-8.94625 -8.86563,-16.22478 -17.55707,-27.44225 -2.80001,-3.82946 -5.52491,-7.67078 -7.70237,-9.83351 1.2085,-1.15603 2.48234,-2.43766 3.67471,-3.70623 1.71163,0.90149 4.05496,1.86857 6.3562,3.58014 4.31134,3.41817 7.17269,8.23064 10.20455,12.73514 9.02396,13.81618 16.24631,18.39025 20.71863,20.64112 5.77715,2.91426 11.83293,4.03246 16.24242,7.167 3.06679,2.40147 5.623,6.10495 6.58771,9.92705 1.25189,5.5451 9.56816,3.67645 8.32766,-1.87121 -1.74384,-5.74755 -5.16756,-11.39162 -10.03815,-14.97118 -5.63746,-3.95554 -12.09561,-5.09221 -17.38189,-7.71529 -9.23039,-4.59405 -14.69079,-13.76571 -16.9478,-17.75964 -2.9899,-5.82031 -5.94378,-11.72918 -10.74043,-16.29564 -1.59447,-1.4853 -3.03675,-2.47899 -4.33307,-3.11144 3.47071,0.40841 8.54372,0.89003 14.17691,2.64893 6.75971,2.94516 9.44739,3.31851 25.0021,19.96467 2.64871,3.43112 7.37316,7.43824 11.24427,11.19622 5.86542,5.85693 13.04545,10.36975 21.14341,12.26178 6.27777,1.48346 8.51479,-7.92575 2.24121,-9.42681 -3.36191,-0.79944 -11.80251,-3.18999 -18.54409,-8.31784 -3.70865,-2.92513 -6.98198,-6.57663 -9.84901,-9.94668 -2.86708,-3.37004 -5.98505,-7.75417 -10.06502,-11.90625 -5.41677,-5.36572 -11.81147,-9.33587 -18.42368,-11.80083 -6.89679,-2.57107 -11.79147,-3.00767 -14.9009,-2.58124 2.50891,-5.48856 6.10219,-17.19419 -3.46387,-25.90178 1.43383,-3.25331 2.61886,-6.46821 3.57394,-9.61543 1.97457,-6.50664 5.23175,-20.59109 1.53686,-33.809822 C 187.55889,73.169869 170.9936,69.810673 169.39586,70.508875 Z m -37.95117,5.312902 C 119.8515,80.44692 108.52615,88.220935 98.601307,98.159622 88.676465,108.09831 80.763495,119.30523 76.343144,130.92022 c -7.230085,18.9979 -6.317388,68.18064 46.430236,44.70425 -34.346397,3.56965 -40.658237,-19.78677 -32.145837,-43.81283 3.109475,-8.77642 9.124568,-17.34164 16.745227,-24.9623 7.62068,-7.62068 16.2735,-13.399209 24.9623,-16.745748 31.64426,-11.637879 46.57574,-3.833923 43.81335,32.145848 C 201.58664,61.657824 144.7383,70.518235 131.44469,75.821777 Z m 16.0631,39.669363 c -7.36376,-0.22251 -16.21769,5.64079 -22.98826,13.28395 -9.02743,10.19087 -14.35077,23.54585 -7.49928,29.85709 6.85148,6.31124 20.13893,-0.69948 29.72273,-10.87376 9.5838,-10.17429 15.46351,-23.51218 7.49928,-29.85657 -1.99106,-1.5861 -4.27988,-2.33654 -6.73447,-2.41071 z"
sodipodi:nodetypes="cssscsssccsssscccsssscsccsccccccccscscscccscsccccsscccccsccccccccscccsssccscscssss" /></svg>

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -93,8 +93,6 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
CFE47DDB2EA56B0200EB6067 /* icons */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = icons;
sourceTree = "<group>";
};
@@ -397,10 +395,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
@@ -414,10 +416,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
@@ -697,10 +703,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -787,10 +790,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;

View File

@@ -1,20 +1,13 @@
import { AxiosResponse } from 'axios'
import { JellyfinCredentials } from '../../types/jellyfin-credentials'
import { AuthenticationResult } from '@jellyfin/sdk/lib/generated-client'
import { useMutation } from '@tanstack/react-query'
import { useJellifyContext } from '../../../providers'
import { JellifyUser } from '../../../types/JellifyUser'
import { capitalize, isUndefined } from 'lodash'
import { isUndefined } from 'lodash'
import { getQuickConnectApi, getUserApi } from '@jellyfin/sdk/lib/utils/api'
import { useNavigation } from '@react-navigation/native'
import LoginStackParamList from '@/src/screens/Login/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { getDeviceId, getDeviceNameSync, getUniqueIdSync } from 'react-native-device-info'
import { name, version } from '../../../../package.json'
const QUICK_CONNECT_HEADER = encodeURIComponent(
`MediaBrowser Device='${getDeviceNameSync()}' DeviceId='${getUniqueIdSync()}'`,
)
export const useInitiateQuickConnect = () => {
const { api } = useJellifyContext()
@@ -23,12 +16,7 @@ export const useInitiateQuickConnect = () => {
mutationFn: async () => {
if (isUndefined(api)) return Promise.reject(new Error('API client is not initialized'))
console.debug('Initiating Quick Connect', QUICK_CONNECT_HEADER)
return await getQuickConnectApi(api!).initiateQuickConnect({
headers: {
Authorization: QUICK_CONNECT_HEADER,
},
})
return await getQuickConnectApi(api).initiateQuickConnect()
},
onError: async (error: Error) => {
console.error('An error occurred initiating Quick Connect', error)
@@ -95,4 +83,16 @@ const useAuthenticateWithQuickConnect = () => {
})
}
const useQuickConnectStatus = () => {
const { api } = useJellifyContext()
return useMutation({
mutationFn: async (secret: string) => {
return await getQuickConnectApi(api!).getQuickConnectState({
secret,
})
},
})
}
export default useAuthenticateWithQuickConnect

View File

@@ -10,6 +10,8 @@ const useGetQuickConnectState = (secret: string) => {
queryFn: async () => {
return await getQuickConnectApi(api!).getQuickConnectState({ secret })
},
gcTime: 0,
staleTime: 0,
})
}

View File

@@ -1,10 +1,12 @@
import React, { useEffect, useState } from 'react'
import React, { useCallback, useEffect, useLayoutEffect } from 'react'
import useAuthenticateWithQuickConnect, {
useInitiateQuickConnect,
} from '../../../api/mutations/quickconnect'
import useGetQuickConnectState from '../../../api/queries/quickconnect'
import { View, Spinner, Button, YStack } from 'tamagui'
import { Text } from '../../Global/helpers/text'
import { View, Spinner, Button, YStack, H6, H5 } from 'tamagui'
import { useFocusEffect, useNavigation } from '@react-navigation/native'
import LoginStackParamList from '@/src/screens/Login/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
// Handles polling, code display, error, and authentication
function QuickConnectDisplay({
@@ -16,36 +18,44 @@ function QuickConnectDisplay({
code: string
onExpired: () => void
}) {
const {
data: stateData,
error: stateError,
isFetching: isStateFetching,
} = useGetQuickConnectState(secret)
const { mutate: authenticate, isPending: isAuthenticating } = useAuthenticateWithQuickConnect()
const {
data: quickConnectData,
error: quickConnectError,
refetch: refetchQuickConnectData,
} = useGetQuickConnectState(secret)
useEffect(() => {}, [secret, code])
// Authenticate when ready
useEffect(() => {
if (stateData?.data.Authenticated && secret) {
if (quickConnectData?.data.Authenticated && secret) {
authenticate(secret)
}
}, [stateData, secret, authenticate])
}, [quickConnectData, secret, authenticate])
// Handle expired/errored code
useEffect(() => {
if (stateError) {
if (quickConnectError) {
onExpired()
}
}, [stateError, onExpired])
}, [quickConnectError, onExpired])
useEffect(() => {
const interval = setInterval(() => {
console.debug(`Checking Quick Connect State: ${JSON.stringify(quickConnectData)}`)
if (quickConnectData?.data.Authenticated) clearInterval(interval)
refetchQuickConnectData()
}, 5000)
return () => clearInterval(interval)
}, [secret])
return (
<View>
<Text>{code}</Text>
{isStateFetching && <Spinner />}
{stateError && (
<View>
<Text color='red'>Code expired. Please try again.</Text>
</View>
)}
<H6>{code}</H6>
{isAuthenticating && <Spinner />}
</View>
)
@@ -53,38 +63,36 @@ function QuickConnectDisplay({
// Initiates quick connect, manages secret/code state, and renders display
export default function QuickConnectInitiator() {
const navigation = useNavigation<NativeStackNavigationProp<LoginStackParamList>>()
const {
mutate: initiateQuickConnect,
reset: resetInitiateQuickConnect,
data: quickConnectData,
isPending: isInitiating,
} = useInitiateQuickConnect()
// When QuickConnect is initiated, set secret and code
useEffect(() => {
initiateQuickConnect()
}, [])
// Reset secret/code to retry
const handleExpired = () => {
const beginQuickConnect = useCallback(() => {
resetInitiateQuickConnect()
initiateQuickConnect()
}
}, [initiateQuickConnect, resetInitiateQuickConnect])
useEffect(() => {
initiateQuickConnect()
return resetInitiateQuickConnect()
})
return (
<YStack>
<Text bold>Quick Connect</Text>
{isInitiating && <Spinner />}
<YStack alignItems='center'>
<H5>Quick Connect</H5>
{quickConnectData?.data.Secret && quickConnectData?.data.Code ? (
<QuickConnectDisplay
secret={quickConnectData.data.Secret}
code={quickConnectData.data.Code}
onExpired={handleExpired}
onExpired={beginQuickConnect}
/>
) : null}
{!quickConnectData?.data.Secret && !isInitiating && (
<Button onPress={() => initiateQuickConnect()}>Retry</Button>
)}
{!quickConnectData?.data.Secret && <Button onPress={beginQuickConnect}>Retry</Button>}
</YStack>
)
}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import { isEmpty, isUndefined } from 'lodash'
import { Input, ListItem, Separator, Spinner, XStack, YGroup, YStack } from 'tamagui'
import { H3, Image, Input, ListItem, Separator, Spinner, XStack, YGroup, YStack } from 'tamagui'
import { SwitchWithLabel } from '../../components/Global/helpers/switch-with-label'
import { H2, Text } from '../../components/Global/helpers/text'
import Button from '../../components/Global/helpers/button'
@@ -62,121 +62,130 @@ export default function ServerAddress({
return (
<SafeAreaView style={{ flex: 1 }}>
<YStack maxHeight={'$19'} flex={1} justifyContent='center'>
<H2 marginHorizontal={'$10'} textAlign='center'>
Connect to Jellyfin
</H2>
</YStack>
<YStack flex={1} justifyContent='center' alignContent='center'>
<YStack maxHeight={'$20'} flex={1} justifyContent='center'>
<H3 marginHorizontal={'$10'} textAlign='center'>
Connect to Jellyfin
</H3>
</YStack>
<YStack marginHorizontal={'$4'} gap={'$4'}>
<XStack alignItems='center'>
{!serverAddressContainsProtocol && (
<Text
borderColor={'$borderColor'}
borderWidth={'$0.5'}
borderRadius={'$4'}
padding={'$2'}
paddingTop={'$2.5'}
width={'$6'}
height={'$4'}
marginRight={'$2'}
color={useHttps ? '$success' : '$borderColor'}
textAlign='center'
verticalAlign={'center'}
>
{useHttps ? HTTPS : HTTP}
</Text>
)}
<YStack marginHorizontal={'$4'} gap={'$4'} flex={1}>
<XStack alignItems='center'>
{!serverAddressContainsProtocol && (
<Text
borderColor={'$borderColor'}
borderWidth={'$0.5'}
borderRadius={'$4'}
padding={'$2'}
paddingTop={'$2.5'}
width={'$6'}
height={'$4'}
marginRight={'$2'}
color={useHttps ? '$success' : '$borderColor'}
textAlign='center'
verticalAlign={'center'}
>
{useHttps ? HTTPS : HTTP}
</Text>
)}
<Input
onChangeText={setServerAddress}
autoCapitalize='none'
autoCorrect={false}
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the server address as screen Records
flex={1}
placeholder='jellyfin.org'
testID='server_address_input'
/>
</XStack>
<Input
onChangeText={setServerAddress}
autoCapitalize='none'
autoCorrect={false}
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the server address as screen Records
flex={1}
placeholder='jellyfin.org'
testID='server_address_input'
returnKeyType='send'
onSubmitEditing={() => {
if (!isUndefined(serverAddress))
connectToServer({ serverAddress, useHttps })
}}
/>
</XStack>
<YGroup
gap={'$2'}
borderColor={'$borderColor'}
borderWidth={'$0.5'}
borderRadius={'$4'}
>
<YGroup.Item>
<ListItem
icon={
<Icon
name={
serverAddressContainsHttps || useHttps
? 'lock-check'
: 'lock-open'
}
color={
serverAddressContainsHttps || useHttps
? '$success'
: '$borderColor'
}
/>
}
title='HTTPS'
subTitle='Use HTTPS to connect to Jellyfin'
disabled={serverAddressContainsProtocol}
>
<SwitchWithLabel
checked={serverAddressContainsHttps || useHttps}
onCheckedChange={(checked) => setUseHttps(checked)}
label={
serverAddressContainsHttps || useHttps
? 'Use HTTPS'
: 'Use HTTP'
<YGroup gap={'$2'} flex={1}>
<YGroup.Item>
<ListItem
icon={
<Icon
name={
serverAddressContainsHttps || useHttps
? 'lock-check'
: 'lock-open'
}
color={
serverAddressContainsHttps || useHttps
? '$success'
: '$borderColor'
}
/>
}
size='$2'
width={100}
/>
</ListItem>
</YGroup.Item>
<Separator />
<YGroup.Item>
<ListItem
icon={
<Icon
name={sendMetrics ? 'bug-check' : 'bug'}
color={sendMetrics ? '$success' : '$borderColor'}
title='HTTPS'
subTitle='Use HTTPS to connect to Jellyfin'
disabled={serverAddressContainsProtocol}
>
<SwitchWithLabel
checked={serverAddressContainsHttps || useHttps}
onCheckedChange={(checked) => setUseHttps(checked)}
label={
serverAddressContainsHttps || useHttps
? 'Use HTTPS'
: 'Use HTTP'
}
size='$2'
width={100}
/>
}
title='Submit Usage and Crash Data'
subTitle='Send anonymized metrics and crash data'
>
<SwitchWithLabel
checked={sendMetrics}
onCheckedChange={(checked) => setSendMetrics(checked)}
label='Send Metrics'
size='$2'
width={100}
/>
</ListItem>
</YGroup.Item>
</YGroup>
</ListItem>
</YGroup.Item>
{isPending ? (
<Spinner />
) : (
<Button
disabled={isEmpty(serverAddress)}
onPress={() => {
if (!isUndefined(serverAddress))
connectToServer({ serverAddress, useHttps })
}}
testID='connect_button'
>
Connect
</Button>
)}
<Separator />
<YGroup.Item>
<ListItem
icon={
<Icon
name={sendMetrics ? 'bug-check' : 'bug'}
color={sendMetrics ? '$success' : '$borderColor'}
/>
}
title='Submit Usage and Crash Data'
subTitle='Send anonymized metrics and crash data'
>
<SwitchWithLabel
checked={sendMetrics}
onCheckedChange={(checked) => setSendMetrics(checked)}
label='Send Metrics'
size='$2'
width={100}
/>
</ListItem>
</YGroup.Item>
<Button
borderWidth={'$1'}
icon={() =>
isPending ? (
<Spinner color='$primary' />
) : (
<Icon name='connection' small color='$primary' />
)
}
color={'$primary'}
borderColor={'$primary'}
disabled={isEmpty(serverAddress) || isPending}
onPress={() => {
if (!isUndefined(serverAddress))
connectToServer({ serverAddress, useHttps })
}}
testID='connect_button'
>
<Text bold color={'$primary'}>
Connect
</Text>
</Button>
</YGroup>
</YStack>
</YStack>
</SafeAreaView>
)

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import _ from 'lodash'
import { H3, H6, Spacer, Spinner, XStack, YStack } from 'tamagui'
import { H2 } from '../../components/Global/helpers/text'
import { H2, Text } from '../../components/Global/helpers/text'
import Button from '../../components/Global/helpers/button'
import { SafeAreaView } from 'react-native-safe-area-context'
import Input from '../../components/Global/helpers/input'
@@ -38,90 +38,104 @@ export default function ServerAuthentication({
return (
<SafeAreaView style={{ flex: 1 }}>
<YStack maxHeight={'$19'} flex={1} justifyContent='center'>
<H3 marginHorizontal={'$2'} textAlign='center'>
{`Sign in to ${server?.name ?? 'Jellyfin'}`}
</H3>
<H6 marginHorizontal={'$2'} textAlign='center'>
{server?.version ?? 'Unknown Jellyfin version'}
</H6>
</YStack>
<YStack marginHorizontal={'$4'}>
<Input
prependElement={<Icon name='human-greeting-variant' color={'$primary'} />}
placeholder='Username'
value={username}
style={
IS_MAESTRO_BUILD ? { backgroundColor: '#000', color: '#000' } : undefined
}
testID='username_input'
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the username as screen Records
onChangeText={(value: string | undefined) => setUsername(value)}
autoCapitalize='none'
autoCorrect={false}
autoComplete='username'
textContentType='username'
importantForAutofill='yes'
returnKeyType='next'
autoFocus
/>
<YStack flex={1} justifyContent='center' alignContent='center'>
<YStack flex={1} maxHeight={'$20'} justifyContent='center' alignContent='center'>
<H3 textAlign='center'>{`Sign in to ${server?.name ?? 'Jellyfin'}`}</H3>
<H6 textAlign='center'>{server?.version ?? 'Unknown Jellyfin version'}</H6>
</YStack>
<YStack marginHorizontal={'$4'} flex={1}>
<Input
prependElement={<Icon name='human-greeting-variant' color={'$primary'} />}
placeholder='Username'
value={username}
style={
IS_MAESTRO_BUILD
? { backgroundColor: '#000', color: '#000' }
: undefined
}
testID='username_input'
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the username as screen Records
onChangeText={(value: string | undefined) => setUsername(value)}
autoCapitalize='none'
autoCorrect={false}
autoComplete='username'
textContentType='username'
importantForAutofill='yes'
returnKeyType='next'
autoFocus
/>
<Spacer />
<Spacer />
<Input
prependElement={<Icon name='lock-outline' color={'$primary'} />}
placeholder='Password'
value={password}
testID='password_input'
style={
IS_MAESTRO_BUILD ? { backgroundColor: '#000', color: '#000' } : undefined
}
onChangeText={(value: string | undefined) => setPassword(value)}
autoCapitalize='none'
autoCorrect={false}
secureTextEntry // Always secure text entry
autoComplete='password'
textContentType='password'
importantForAutofill='yes'
returnKeyType='go'
/>
<Input
prependElement={<Icon name='lock-outline' color={'$primary'} />}
placeholder='Password'
value={password}
testID='password_input'
style={
IS_MAESTRO_BUILD
? { backgroundColor: '#000', color: '#000' }
: undefined
}
onChangeText={(value: string | undefined) => setPassword(value)}
autoCapitalize='none'
autoCorrect={false}
secureTextEntry // Always secure text entry
autoComplete='password'
textContentType='password'
importantForAutofill='yes'
returnKeyType='go'
/>
<Spacer />
<Spacer />
<QuickConnect />
<Spacer />
<XStack justifyContent='space-between'>
<Button
borderWidth={'$1'}
borderColor={'$primary'}
disabled={_.isEmpty(username) || isPending}
icon={() =>
isPending ? (
<Spinner color='$primary' />
) : (
<Icon name='chevron-right' small color='$primary' />
)
}
testID='sign_in_button'
onPress={() => {
if (!_.isUndefined(username)) {
console.log(`Signing in...`)
authenticateUserByName({ username, password })
}
}}
>
<Text bold color={'$primary'}>
Sign in
</Text>
</Button>
<Spacer />
<YStack flex={1} marginVertical={'$4'}>
<QuickConnect />
</YStack>
<Spacer />
<Button
borderWidth={'$1'}
borderColor={'$borderColor'}
marginVertical={0}
icon={() => <Icon name='chevron-left' small />}
icon={() => <Icon name='chevron-left' small color='$borderColor' />}
bordered={0}
onPress={() => {
navigation.popTo('ServerAddress', undefined)
}}
>
Switch Server
<Text bold color={'$borderColor'}>
Switch Server
</Text>
</Button>
{isPending ? (
<Spinner />
) : (
<Button
marginVertical={0}
disabled={_.isEmpty(username) || isPending}
icon={() => <Icon name='chevron-right' small />}
testID='sign_in_button'
onPress={() => {
if (!_.isUndefined(username)) {
console.log(`Signing in...`)
authenticateUserByName({ username, password })
}
}}
>
Sign in
</Button>
)}
</XStack>
</YStack>
{/* <Toast /> */}
</YStack>
</SafeAreaView>

View File

@@ -6,6 +6,7 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import LibrarySelector from '../../components/Global/components/library-selector'
import LoginStackParamList from './types'
import { useNavigation } from '@react-navigation/native'
import { useInitiateQuickConnect } from '../../api/mutations/quickconnect'
export default function ServerLibrary({
navigation,
@@ -16,6 +17,8 @@ export default function ServerLibrary({
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const initiateQuickConnect = useInitiateQuickConnect()
const handleLibrarySelected = (
libraryId: string,
selectedLibrary: BaseItemDto,
@@ -32,9 +35,8 @@ export default function ServerLibrary({
}
const handleCancel = () => {
navigation.navigate('ServerAuthentication', undefined, {
pop: true,
})
initiateQuickConnect.reset()
navigation.popTo('ServerAuthentication', undefined)
}
return (