This commit is contained in:
2026-03-01 11:23:48 +01:00
parent eeeef2674a
commit 9b418a11c2
5 changed files with 414 additions and 40 deletions

View File

@@ -1,38 +1,44 @@
import json
DEFAULT_STATE = {"text": "", "image_url": ""}
def _read_state():
try:
with open("/app/state.json", "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return dict(DEFAULT_STATE)
def _write_state(state):
with open("/app/state.json", "w") as f:
json.dump(state, f)
def main(args):
route = args.get("route")
# Verify file exists, if not create it with default content
try:
with open("/app/state.json", "r") as f:
pass
except FileNotFoundError:
with open("/app/state.json", "w") as f:
json.dump({"text": ""}, f)
f.flush()
body = args.get("body")
body = json.loads(body) if body else {}
if route.startswith("push"):
if route == "push_text":
with open("/app/state.json", "r") as f:
current = json.load(f)
current = _read_state()
current["text"] = body.get("text", "")
with open("/app/state.json", "w") as f:
json.dump(current, f)
_write_state(current)
return {"status": "success"}
elif route == "push_image_url":
current = _read_state()
current["image_url"] = body.get("image_url", "")
_write_state(current)
return {"status": "success"}
else:
return {"status": "error", "message": "Unknown push route"}
elif route.startswith("pull"):
current = _read_state()
if route == "pull_text":
with open("/app/state.json", "r") as f:
current = json.load(f)
return {"status": "success", "text": current.get("text", "")}
elif route == "pull_image_url":
return {"status": "success", "image_url": current.get("image_url", "")}
elif route == "pull_full":
with open("/app/state.json", "r") as f:
current = json.load(f)
return {"status": "success", "state": current}
else:
return {"status": "error", "message": "Unknown pull route"}

View File

@@ -10,7 +10,9 @@
},
"dependencies": {
"expo": "~54.0.33",
"expo-image-picker": "^55.0.10",
"expo-status-bar": "~3.0.9",
"minio": "^8.0.7",
"react": "19.1.0",
"react-native": "0.81.5"
},

188
mobile/pnpm-lock.yaml generated
View File

@@ -11,9 +11,15 @@ importers:
expo:
specifier: ~54.0.33
version: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-image-picker:
specifier: ^55.0.10
version: 55.0.10(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
expo-status-bar:
specifier: ~3.0.9
version: 3.0.9(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
minio:
specifier: ^8.0.7
version: 8.0.7
react:
specifier: 19.1.0
version: 19.1.0
@@ -894,6 +900,9 @@ packages:
async-limiter@1.0.1:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
babel-jest@29.7.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -981,6 +990,9 @@ packages:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
block-stream2@2.1.0:
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
bplist-creator@0.1.0:
resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==}
@@ -1006,6 +1018,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browser-or-node@2.1.1:
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
browserslist@4.28.1:
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -1014,6 +1029,10 @@ packages:
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
buffer-crc32@1.0.0:
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
engines: {node: '>=8.0.0'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -1159,6 +1178,10 @@ packages:
supports-color:
optional: true
decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@@ -1250,6 +1273,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
expo-asset@12.0.12:
resolution: {integrity: sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==}
peerDependencies:
@@ -1276,6 +1302,16 @@ packages:
react: '*'
react-native: '*'
expo-image-loader@55.0.0:
resolution: {integrity: sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==}
peerDependencies:
expo: '*'
expo-image-picker@55.0.10:
resolution: {integrity: sha512-uspDWjNBNjUD//MLzu7oEtT9cuNgW5pd6HxPfAJffRIdkdCUCYxLYXRp5pfB6JtW640eCdvm2QEQrQC172Y62g==}
peerDependencies:
expo: '*'
expo-keep-awake@15.0.8:
resolution: {integrity: sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==}
peerDependencies:
@@ -1325,6 +1361,13 @@ packages:
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-xml-builder@1.0.0:
resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==}
fast-xml-parser@5.4.1:
resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==}
hasBin: true
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
@@ -1341,6 +1384,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
filter-obj@1.1.0:
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
engines: {node: '>=0.10.0'}
finalhandler@1.1.2:
resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
engines: {node: '>= 0.8'}
@@ -1472,6 +1519,10 @@ packages:
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
ipaddr.js@2.3.0:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'}
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
@@ -1669,6 +1720,9 @@ packages:
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
log-symbols@2.2.0:
resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
engines: {node: '>=4'}
@@ -1858,6 +1912,10 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minio@8.0.7:
resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==}
engines: {node: ^16 || ^18 || >=20}
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -2069,6 +2127,10 @@ packages:
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
hasBin: true
query-string@7.1.3:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'}
queue@6.0.2:
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
@@ -2111,6 +2173,10 @@ packages:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
regenerate-unicode-properties@10.2.2:
resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
engines: {node: '>=4'}
@@ -2250,6 +2316,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
split-on-first@1.1.0:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -2276,10 +2346,23 @@ packages:
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
engines: {node: '>= 0.10.0'}
stream-chain@2.2.5:
resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
stream-json@1.9.1:
resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@5.2.0:
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
engines: {node: '>=6'}
@@ -2292,6 +2375,9 @@ packages:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strnum@2.2.0:
resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==}
structured-headers@0.4.1:
resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
@@ -2347,6 +2433,9 @@ packages:
throat@5.0.0:
resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==}
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -2415,6 +2504,9 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
@@ -2513,6 +2605,10 @@ packages:
resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
engines: {node: '>=4.0.0'}
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
@@ -3786,6 +3882,8 @@ snapshots:
async-limiter@1.0.1: {}
async@3.2.6: {}
babel-jest@29.7.0(@babel/core@7.29.0):
dependencies:
'@babel/core': 7.29.0
@@ -3927,6 +4025,10 @@ snapshots:
big-integer@1.6.52: {}
block-stream2@2.1.0:
dependencies:
readable-stream: 3.6.2
bplist-creator@0.1.0:
dependencies:
stream-buffers: 2.2.0
@@ -3956,6 +4058,8 @@ snapshots:
dependencies:
fill-range: 7.1.1
browser-or-node@2.1.1: {}
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.10.0
@@ -3968,6 +4072,8 @@ snapshots:
dependencies:
node-int64: 0.4.0
buffer-crc32@1.0.0: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -4107,6 +4213,8 @@ snapshots:
dependencies:
ms: 2.1.3
decode-uri-component@0.2.2: {}
deep-extend@0.6.0: {}
deepmerge@4.3.1: {}
@@ -4161,6 +4269,8 @@ snapshots:
event-target-shim@5.0.1: {}
eventemitter3@5.0.4: {}
expo-asset@12.0.12(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
'@expo/image-utils': 0.8.12
@@ -4192,6 +4302,15 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
expo-image-loader@55.0.0(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)):
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-image-picker@55.0.10(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)):
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-image-loader: 55.0.0(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
expo-keep-awake@15.0.8(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -4256,6 +4375,13 @@ snapshots:
fast-json-stable-stringify@2.1.0: {}
fast-xml-builder@1.0.0: {}
fast-xml-parser@5.4.1:
dependencies:
fast-xml-builder: 1.0.0
strnum: 2.2.0
fb-watchman@2.0.2:
dependencies:
bser: 2.1.1
@@ -4268,6 +4394,8 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
filter-obj@1.1.0: {}
finalhandler@1.1.2:
dependencies:
debug: 2.6.9
@@ -4393,6 +4521,8 @@ snapshots:
dependencies:
loose-envify: 1.4.0
ipaddr.js@2.3.0: {}
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
@@ -4584,6 +4714,8 @@ snapshots:
lodash.throttle@4.1.1: {}
lodash@4.17.23: {}
log-symbols@2.2.0:
dependencies:
chalk: 2.4.2
@@ -4994,6 +5126,22 @@ snapshots:
minimist@1.2.8: {}
minio@8.0.7:
dependencies:
async: 3.2.6
block-stream2: 2.1.0
browser-or-node: 2.1.1
buffer-crc32: 1.0.0
eventemitter3: 5.0.4
fast-xml-parser: 5.4.1
ipaddr.js: 2.3.0
lodash: 4.17.23
mime-types: 2.1.35
query-string: 7.1.3
stream-json: 1.9.1
through2: 4.0.2
xml2js: 0.6.2
minipass@7.1.3: {}
minizlib@3.1.0:
@@ -5169,6 +5317,13 @@ snapshots:
qrcode-terminal@0.11.0: {}
query-string@7.1.3:
dependencies:
decode-uri-component: 0.2.2
filter-obj: 1.1.0
split-on-first: 1.1.0
strict-uri-encode: 2.0.0
queue@6.0.2:
dependencies:
inherits: 2.0.4
@@ -5248,6 +5403,12 @@ snapshots:
react@19.1.0: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
regenerate-unicode-properties@10.2.2:
dependencies:
regenerate: 1.4.2
@@ -5380,6 +5541,8 @@ snapshots:
source-map@0.6.1: {}
split-on-first@1.1.0: {}
sprintf-js@1.0.3: {}
stack-utils@2.0.6:
@@ -5398,12 +5561,24 @@ snapshots:
stream-buffers@2.2.0: {}
stream-chain@2.2.5: {}
stream-json@1.9.1:
dependencies:
stream-chain: 2.2.5
strict-uri-encode@2.0.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@5.2.0:
dependencies:
ansi-regex: 4.1.1
@@ -5414,6 +5589,8 @@ snapshots:
strip-json-comments@2.0.1: {}
strnum@2.2.0: {}
structured-headers@0.4.1: {}
sucrase@3.35.1:
@@ -5481,6 +5658,10 @@ snapshots:
throat@5.0.0: {}
through2@4.0.2:
dependencies:
readable-stream: 3.6.2
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -5527,6 +5708,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
uuid@7.0.3: {}
@@ -5592,6 +5775,11 @@ snapshots:
sax: 1.4.4
xmlbuilder: 11.0.1
xml2js@0.6.2:
dependencies:
sax: 1.4.4
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
xmlbuilder@15.1.1: {}

View File

@@ -1,9 +1,24 @@
import * as ImagePicker from "expo-image-picker";
import * as Minio from "minio";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import { TextInput } from "react-native";
import { ActivityIndicator, Button, Image, StyleSheet, Text, TextInput, View } from "react-native";
const minioClient = new Minio.Client({
endPoint: "content2.reversed.dev",
port: 443,
useSSL: true,
accessKey: "tv-control",
secretKey: "0gEjOlnVAECrqJx5Nd77y7Hhouc5faVf0WttjcRH",
});
const bucket = "tv-control";
const BASE_URL =
"https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329";
export function IndexPage() {
const [text, setText] = useState("");
const [uploading, setUploading] = useState(false);
const [previewUri, setPreviewUri] = useState<string | null>(null);
const handleSend = () => {
if (text.trim() === "") {
@@ -11,14 +26,12 @@ export function IndexPage() {
return;
}
fetch("https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/push_text", {
fetch(`${BASE_URL}/push_text`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
})
.then((response) => response.json())
.then((r) => r.json())
.then((data) => {
if (data.status === "success") {
alert("Text sent successfully!");
@@ -32,18 +45,100 @@ export function IndexPage() {
});
};
const handlePullText = () => {
fetch(`${BASE_URL}/pull_text`)
.then((r) => r.json())
.then((data) => {
alert(data.text ?? "No text received.");
})
.catch((error) => {
console.error("Error pulling text:", error);
alert("Error pulling text.");
});
};
const handleTakePhoto = async () => {
const { granted } = await ImagePicker.requestCameraPermissionsAsync();
if (!granted) {
alert("Camera permission is required to take photos.");
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: "images",
quality: 0.8,
});
if (result.canceled) return;
const asset = result.assets[0];
setPreviewUri(asset.uri);
setUploading(true);
try {
const fileName = `tv-image-${Date.now()}.jpg`;
// Get a presigned URL for PUT upload (pure signing — no HTTP call)
const presignedUrl = await minioClient.presignedPutObject(
bucket,
fileName,
60 * 60
);
// Upload the image blob using native fetch
const blob = await fetch(asset.uri).then((r) => r.blob());
await fetch(presignedUrl, {
method: "PUT",
body: blob,
headers: { "Content-Type": "image/jpeg" },
});
const imageUrl = `https://content2.reversed.dev/${bucket}/${fileName}`;
// Push the public URL to the server
await fetch(`${BASE_URL}/push_image_url`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image_url: imageUrl }),
});
alert("Image uploaded and sent to TV!");
} catch (error) {
console.error("Upload error:", error);
alert("Failed to upload image.");
} finally {
setUploading(false);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Update Title</Text>
<Text style={styles.subtitle}>You typed "{text}"</Text>
<Text style={styles.title}>TV Control</Text>
<View>
<View style={styles.section}>
<Text style={styles.sectionLabel}>Text</Text>
<TextInput
style={styles.input}
placeholder="Type something..."
value={text}
onChangeText={setText}
/>
<Button title="Send" onPress={handleSend} />
<View style={styles.row}>
<Button title="Send Text" onPress={handleSend} />
<Button title="Pull Text" onPress={handlePullText} />
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}>Image</Text>
{previewUri && (
<Image source={{ uri: previewUri }} style={styles.preview} />
)}
{uploading ? (
<ActivityIndicator size="large" style={{ marginTop: 12 }} />
) : (
<Button title="Take Photo & Send to TV" onPress={handleTakePhoto} />
)}
</View>
</View>
);
@@ -55,14 +150,41 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "center",
backgroundColor: "#f9f9f9",
padding: 24,
gap: 24,
},
title: {
fontSize: 28,
fontWeight: "700",
marginBottom: 8,
},
subtitle: {
section: {
width: "100%",
gap: 8,
},
sectionLabel: {
fontSize: 14,
fontWeight: "600",
color: "#555",
textTransform: "uppercase",
letterSpacing: 0.5,
},
input: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 10,
fontSize: 16,
color: "#666",
backgroundColor: "#fff",
},
row: {
flexDirection: "row",
gap: 8,
},
preview: {
width: "100%",
height: 200,
borderRadius: 8,
resizeMode: "cover",
},
});

View File

@@ -4,6 +4,8 @@ function App() {
const [screenStatus, setScreenStatus] = useState<
"notfullscreen" | "fullscreen"
>("notfullscreen");
const [text, setText] = useState("");
const [imageUrl, setImageUrl] = useState("");
useEffect(() => {
const handleFullscreenChange = () => {
@@ -21,14 +23,48 @@ function App() {
};
}, []);
useEffect(() => {
if (screenStatus === "fullscreen") {
const handlePullState = () => {
fetch(
"https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/pull_full",
)
.then((response) => response.json())
.then((data) => {
setText(data.state?.text ?? "");
setImageUrl(data.state?.image_url ?? "");
})
.catch((error) => {
console.error("Error pulling state:", error);
});
};
handlePullState(); // Initial pull when entering fullscreen
const interval = setInterval(handlePullState, 5000); // Poll every 5 seconds
return () => clearInterval(interval);
}
}, [screenStatus]);
return (
<>
{screenStatus === "notfullscreen" ? (
<div className="flex flex-col items-center gap-8 px-8 text-center">
<div className="flex flex-col items-center gap-2">
<div className="w-16 h-16 rounded-2xl bg-gray-800 border border-gray-700 flex items-center justify-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="w-8 h-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 8V6a2 2 0 012-2h8a2 2 0 012 2v2M6 16v2a2 2 0 002 2h8a2 2 0 002-2v-2M3 12h18" />
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-8 h-8 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 8V6a2 2 0 012-2h8a2 2 0 012 2v2M6 16v2a2 2 0 002 2h8a2 2 0 002-2v-2M3 12h18"
/>
</svg>
</div>
<h1 className="text-3xl font-semibold tracking-tight text-white">
@@ -43,17 +79,37 @@ function App() {
onClick={() => document.documentElement.requestFullscreen()}
className="group flex items-center gap-2 bg-white text-gray-900 font-medium px-6 py-3 rounded-xl hover:bg-gray-200 active:scale-95 transition-all duration-150 cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-5h-4m4 0v4m0-4l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 8V4m0 0h4M4 4l5 5m11-5h-4m4 0v4m0-4l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
/>
</svg>
Go Fullscreen
</button>
</div>
) : (
<div className="flex flex-col items-center gap-4 text-center">
<h1 className="text-4xl font-bold tracking-tight text-white">
Fullscreen Active, yay
</h1>
<div className="flex flex-col items-center gap-6 text-center w-full h-full justify-center">
{imageUrl && (
<img
src={imageUrl}
alt="TV display"
className="max-w-full max-h-[70vh] rounded-2xl object-contain shadow-2xl"
/>
)}
{text && (
<h1 className="text-4xl font-bold tracking-tight text-white">
{text}
</h1>
)}
</div>
)}
</>