feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run
BIN
frontend/public/app-icons/auto-care-192.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
frontend/public/app-icons/auto-care-512.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/public/app-icons/auto-care-apple.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
frontend/public/app-icons/autonomous-dev-studio-192.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/app-icons/autonomous-dev-studio-512.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/app-icons/autonomous-dev-studio-apple.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
frontend/public/app-icons/codex-commander-192.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
frontend/public/app-icons/codex-commander-512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/app-icons/codex-commander-apple.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
frontend/public/app-icons/knowledge-graph-192.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
frontend/public/app-icons/knowledge-graph-512.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
frontend/public/app-icons/knowledge-graph-apple.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
frontend/public/app-icons/loutre-app-store-192.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
frontend/public/app-icons/loutre-app-store-512.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/public/app-icons/loutre-app-store-apple.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
frontend/public/app-icons/loutre-brain-192.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
frontend/public/app-icons/loutre-brain-512.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
frontend/public/app-icons/loutre-brain-apple.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/public/app-icons/loutre-finance-192.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
frontend/public/app-icons/loutre-finance-512.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/public/app-icons/loutre-finance-apple.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
frontend/public/app-icons/loutre-home-192.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/app-icons/loutre-home-512.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
frontend/public/app-icons/loutre-home-apple.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
frontend/public/app-icons/loutre-os-192.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
frontend/public/app-icons/loutre-os-512.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/public/app-icons/loutre-os-apple.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/app-icons/nasflix-ai-media-curator-192.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
frontend/public/app-icons/nasflix-ai-media-curator-512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/app-icons/nasflix-ai-media-curator-apple.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/public/app-icons/nasflix-media-health-192.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
frontend/public/app-icons/nasflix-media-health-512.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/public/app-icons/nasflix-media-health-apple.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/public/app-icons/network-guardian-192.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
frontend/public/app-icons/network-guardian-512.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/public/app-icons/network-guardian-apple.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
frontend/public/app-icons/pi-doctor-192.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/public/app-icons/pi-doctor-512.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/app-icons/pi-doctor-apple.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/public/app-icons/print-pilot-192.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
frontend/public/app-icons/print-pilot-512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/app-icons/print-pilot-apple.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
frontend/public/app-icons/pwa-factory-192.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/public/app-icons/pwa-factory-512.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/public/app-icons/pwa-factory-apple.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
frontend/public/app-icons/reverse-proxy-manager-192.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/public/app-icons/reverse-proxy-manager-512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/app-icons/reverse-proxy-manager-apple.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
frontend/public/app-icons/smart-inventory-192.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
frontend/public/app-icons/smart-inventory-512.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/public/app-icons/smart-inventory-apple.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
frontend/public/app-icons/trip-organizer-192.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/public/app-icons/trip-organizer-512.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
frontend/public/app-icons/trip-organizer-apple.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
7
frontend/public/browser-touchpad-math.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function relativePointerPosition(pointer,dx,dy,scaleX,scaleY,speed,width,height){
|
||||
const clamp=(value,min,max)=>Math.min(max,Math.max(min,value))
|
||||
return {
|
||||
x:clamp(pointer.x+dx*scaleX*speed,0,Math.max(0,width-1)),
|
||||
y:clamp(pointer.y+dy*scaleY*speed,0,Math.max(0,height-1)),
|
||||
}
|
||||
}
|
||||
154
frontend/public/browser-touchpad.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import UI from '/browser/app/ui.js'
|
||||
import {relativePointerPosition} from '/browser-touchpad-math.js'
|
||||
|
||||
const POINTER_SPEED=1.35
|
||||
const MOVE_THRESHOLD=6
|
||||
const LONG_PRESS_DELAY=560
|
||||
const DOUBLE_TAP_DELAY=320
|
||||
const SCROLL_STEP=18
|
||||
|
||||
let installedCanvas=null
|
||||
let touchSurface=null
|
||||
let pointer={x:0,y:0}
|
||||
let primaryTouch=null
|
||||
let origin=null
|
||||
let lastPoint=null
|
||||
let moved=false
|
||||
let longPressed=false
|
||||
let dragging=false
|
||||
let longPressTimer=null
|
||||
let lastTapAt=0
|
||||
let secondTap=false
|
||||
let scrollY=null
|
||||
let scrollRemainder=0
|
||||
|
||||
function rfb(){return UI.rfb}
|
||||
function canvas(){return rfb()?._canvas||null}
|
||||
function bounds(){const target=canvas();return target?{target,rect:target.getBoundingClientRect()}:null}
|
||||
function syncPointer(){
|
||||
const remote=rfb()?._mousePos
|
||||
const target=canvas()
|
||||
if(Number.isFinite(remote?.x)&&Number.isFinite(remote?.y)){pointer={x:remote.x,y:remote.y}}
|
||||
else if(target){pointer={x:target.width/2,y:target.height/2}}
|
||||
}
|
||||
function movePointer(dx,dy){
|
||||
const current=rfb(),geometry=bounds()
|
||||
if(!current||!geometry||!geometry.rect.width||!geometry.rect.height)return
|
||||
const scaleX=geometry.target.width/geometry.rect.width
|
||||
const scaleY=geometry.target.height/geometry.rect.height
|
||||
pointer=relativePointerPosition(pointer,dx,dy,scaleX,scaleY,POINTER_SPEED,geometry.target.width,geometry.target.height)
|
||||
current._handleMouseMove(pointer.x,pointer.y)
|
||||
current._cursor?.move(geometry.rect.left+pointer.x/scaleX,geometry.rect.top+pointer.y/scaleY)
|
||||
}
|
||||
function click(mask=1){
|
||||
const current=rfb()
|
||||
if(!current)return
|
||||
syncPointer()
|
||||
current._handleMouseButton(pointer.x,pointer.y,mask)
|
||||
current._handleMouseButton(pointer.x,pointer.y,0)
|
||||
}
|
||||
function beginDrag(){
|
||||
if(dragging||!rfb())return
|
||||
dragging=true
|
||||
rfb()._handleMouseButton(pointer.x,pointer.y,1)
|
||||
}
|
||||
function endDrag(){
|
||||
if(!dragging||!rfb())return
|
||||
rfb()._handleMouseButton(pointer.x,pointer.y,0)
|
||||
dragging=false
|
||||
}
|
||||
function clearLongPress(){if(longPressTimer!==null){clearTimeout(longPressTimer);longPressTimer=null}}
|
||||
function touchAverage(touches){let total=0;for(const touch of touches)total+=touch.clientY;return touches.length?total/touches.length:0}
|
||||
function wheel(direction){
|
||||
const current=rfb()
|
||||
if(!current)return
|
||||
const mask=direction>0?8:16
|
||||
current._handleMouseButton(pointer.x,pointer.y,mask)
|
||||
current._handleMouseButton(pointer.x,pointer.y,0)
|
||||
}
|
||||
function consume(event){event.preventDefault();event.stopImmediatePropagation()}
|
||||
function onTouchStart(event){
|
||||
consume(event)
|
||||
canvas()?.focus()
|
||||
if(event.touches.length>=2){
|
||||
clearLongPress();endDrag();primaryTouch=null;scrollY=touchAverage(event.touches);scrollRemainder=0;return
|
||||
}
|
||||
const touch=event.changedTouches[0]
|
||||
if(!touch)return
|
||||
syncPointer()
|
||||
primaryTouch=touch.identifier
|
||||
origin={x:touch.clientX,y:touch.clientY}
|
||||
lastPoint={...origin}
|
||||
moved=false
|
||||
longPressed=false
|
||||
secondTap=Date.now()-lastTapAt<DOUBLE_TAP_DELAY
|
||||
clearLongPress()
|
||||
if(!secondTap)longPressTimer=setTimeout(()=>{longPressed=true;click(4);navigator.vibrate?.(25)},LONG_PRESS_DELAY)
|
||||
}
|
||||
function onTouchMove(event){
|
||||
consume(event)
|
||||
if(event.touches.length>=2){
|
||||
clearLongPress()
|
||||
const next=touchAverage(event.touches)
|
||||
if(scrollY!==null){
|
||||
scrollRemainder+=next-scrollY
|
||||
while(Math.abs(scrollRemainder)>=SCROLL_STEP){const direction=Math.sign(scrollRemainder);wheel(direction);scrollRemainder-=direction*SCROLL_STEP}
|
||||
}
|
||||
scrollY=next
|
||||
return
|
||||
}
|
||||
const touch=[...event.touches].find(item=>item.identifier===primaryTouch)
|
||||
if(!touch||!lastPoint||!origin)return
|
||||
const total=Math.hypot(touch.clientX-origin.x,touch.clientY-origin.y)
|
||||
if(total>MOVE_THRESHOLD){moved=true;clearLongPress();if(secondTap)beginDrag()}
|
||||
movePointer(touch.clientX-lastPoint.x,touch.clientY-lastPoint.y)
|
||||
lastPoint={x:touch.clientX,y:touch.clientY}
|
||||
}
|
||||
function onTouchEnd(event){
|
||||
consume(event)
|
||||
if(event.touches.length>=2){scrollY=touchAverage(event.touches);return}
|
||||
if(event.touches.length===1){
|
||||
scrollY=null
|
||||
const touch=event.touches[0]
|
||||
primaryTouch=touch.identifier;origin={x:touch.clientX,y:touch.clientY};lastPoint={...origin};return
|
||||
}
|
||||
clearLongPress();endDrag()
|
||||
if(!moved&&!longPressed){click(1);lastTapAt=Date.now()}else if(moved){lastTapAt=0}
|
||||
primaryTouch=null;origin=null;lastPoint=null;scrollY=null;scrollRemainder=0;secondTap=false
|
||||
}
|
||||
function onTouchCancel(event){
|
||||
consume(event);clearLongPress();endDrag()
|
||||
primaryTouch=null;origin=null;lastPoint=null;scrollY=null;scrollRemainder=0;secondTap=false;moved=false
|
||||
}
|
||||
function installTouchSurface(target){
|
||||
if(!touchSurface){
|
||||
touchSurface=document.createElement('div')
|
||||
touchSurface.id='codexpi-relative-touch-surface'
|
||||
touchSurface.setAttribute('aria-label','Pavé tactile relatif')
|
||||
// noVNC draws its software cursor at z-index 65535. Stay immediately
|
||||
// below it so touches hit this surface while the pointer remains visible.
|
||||
Object.assign(touchSurface.style,{position:'fixed',zIndex:'65534',background:'transparent',touchAction:'none',userSelect:'none'})
|
||||
touchSurface.addEventListener('touchstart',onTouchStart,{capture:true,passive:false})
|
||||
touchSurface.addEventListener('touchmove',onTouchMove,{capture:true,passive:false})
|
||||
touchSurface.addEventListener('touchend',onTouchEnd,{capture:true,passive:false})
|
||||
touchSurface.addEventListener('touchcancel',onTouchCancel,{capture:true,passive:false})
|
||||
document.body.appendChild(touchSurface)
|
||||
}
|
||||
const rect=target.getBoundingClientRect()
|
||||
Object.assign(touchSurface.style,{left:`${rect.left}px`,top:`${rect.top}px`,width:`${rect.width}px`,height:`${rect.height}px`,display:rect.width&&rect.height?'block':'none'})
|
||||
}
|
||||
function install(){
|
||||
const target=canvas()
|
||||
if(!target){if(touchSurface)touchSurface.style.display='none';return}
|
||||
target.style.touchAction='none'
|
||||
target.style.userSelect='none'
|
||||
installTouchSurface(target)
|
||||
if(target!==installedCanvas){
|
||||
installedCanvas=target
|
||||
syncPointer()
|
||||
}
|
||||
}
|
||||
|
||||
window.codexpiTouchpad={click,install}
|
||||
window.setInterval(install,500)
|
||||
install()
|
||||
BIN
frontend/public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
frontend/public/icons/icon-192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/icons/icon-maskable-512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
6
frontend/public/icons/icon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="112" fill="#111316"/>
|
||||
<rect x="74" y="74" width="364" height="364" rx="92" fill="#e8e9e4"/>
|
||||
<path d="M173 286c0-61 36-101 88-101 34 0 62 16 78 44l-45 26c-8-15-19-22-34-22-23 0-39 20-39 53 0 34 16 54 40 54 16 0 28-8 35-24l46 24c-16 31-46 49-82 49-52 0-87-40-87-103Z" fill="#111316"/>
|
||||
<circle cx="354" cy="167" r="22" fill="#5ab978"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 446 B |
16
frontend/public/manifest.webmanifest
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "CodexPi",
|
||||
"short_name": "CodexPi",
|
||||
"description": "Atelier Codex privé hébergé sur Raspberry Pi",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b0c0e",
|
||||
"theme_color": "#0b0c0e",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||
58
frontend/public/sw.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const CACHE = 'codexpi-shell-v18'
|
||||
const SHELL = ['/', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png', '/icons/apple-touch-icon.png']
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(SHELL)).then(() => self.skipWaiting()))
|
||||
})
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim()))
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const request = event.request
|
||||
const url = new URL(request.url)
|
||||
if (request.method !== 'GET' || url.origin !== location.origin || url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws/')) return
|
||||
if (url.pathname === '/version.json' || url.pathname === '/sw.js') {
|
||||
event.respondWith(fetch(request, {cache: 'no-store'}))
|
||||
return
|
||||
}
|
||||
if (request.mode === 'navigate') {
|
||||
event.respondWith(fetch(request).then(response => {
|
||||
const copy = response.clone()
|
||||
caches.open(CACHE).then(cache => cache.put('/', copy))
|
||||
return response
|
||||
}).catch(() => caches.match('/')))
|
||||
return
|
||||
}
|
||||
event.respondWith(caches.match(request).then(cached => cached || fetch(request).then(response => {
|
||||
if (response.ok) caches.open(CACHE).then(cache => cache.put(request, response.clone()))
|
||||
return response
|
||||
})))
|
||||
})
|
||||
|
||||
self.addEventListener('push', event => {
|
||||
let message = {}
|
||||
try { message = event.data ? event.data.json() : {} } catch {}
|
||||
event.waitUntil(self.registration.showNotification(message.title || 'Tâche CodexPi terminée', {
|
||||
body: message.body || 'Ouvrez CodexPi pour consulter le résultat.',
|
||||
icon: '/icons/icon-192.png', badge: '/icons/icon-192.png',
|
||||
tag: message.tag || 'codexpi-task', data: {url: message.url || '/'},
|
||||
}))
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
event.notification.close()
|
||||
event.waitUntil((async () => {
|
||||
const target = new URL(event.notification.data?.url || '/', self.location.origin)
|
||||
if (target.origin !== self.location.origin) return
|
||||
const windows = await self.clients.matchAll({type: 'window', includeUncontrolled: true})
|
||||
for (const client of windows) {
|
||||
if (new URL(client.url).origin === target.origin && 'navigate' in client) {
|
||||
const navigated = await client.navigate(target.href)
|
||||
if (navigated) { await navigated.focus(); return }
|
||||
}
|
||||
}
|
||||
await self.clients.openWindow(target.href)
|
||||
})())
|
||||
})
|
||||