Fujimod
브라우저에서 카메라 펌웨어를 시험하고 Fujimod SD 부팅 파일을 만든다
작은 경험을 한 칸씩 채워 사진·글을 스크랩북처럼 저장, 단계·속도·연속성·리더보드 없이 사용
Fill in grids of small experiences, one square at a time, and keep the photos and words as a scrapbook. No steps, pace, streaks or leaderboards.
라이프스타일TypeScript · PLpgSQLGitHub @paulkinderman온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Moments over metrics. Fill in grids of small experiences, one square at a time, and keep the photos and words as a scrapbook.
This is v2: everything in v1 (Sign in with Apple, sync between phones through Supabase, the chapter cover, export and account deletion) plus the Atlas:
admin/ to hide grids and suspend sharing. Two hides in 30 days suspends sharing automatically.And friends:
And grids done together:
The phone's SQLite database stays the source of truth, so everything works offline; the Atlas shows what it last fetched.
npm install
npx expo start
Everything in v1 runs in Expo Go on an iPhone, including Sign in with Apple, as long as Expo Go's bundle id host.exp.Exponent is listed as an Apple client id in Supabase. Google sign-in is the one exception: it needs a development build, and its button stays hidden in Expo Go. To make a development build:
npx expo run:ios # needs Xcode on a Mac
npx expo run:android # needs Android Studio
Copy .env.example to .env and fill in the Supabase URL and anon (publishable) key. Leave the Google ids blank for Apple-only on iOS. Without a .env, a development build offers "Continue on this phone only" so the app can be tried with no backend.
Backend, once per Supabase project:
supabase link --project-ref <ref>
supabase db push # supabase/migrations (then npm run gen:types to refresh src/db/database.types.ts)
supabase functions deploy delete-account
The admin site has its own setup; see admin/README.md.
In the Supabase dashboard, enable the Apple provider (client id app.airstrive.mobile) and, if used, Google (web client id, with the iOS client id under authorized client ids).
eas.json has a production profile: store distribution, build numbers kept by EAS and bumped on every build, and the production environment variables from expo.dev. The marketing version is version in app.json.
EXPO_PUBLIC_SUPABASE_URL, EXPO_PUBLIC_SUPABASE_ANON_KEY and the Google client ids for the production environment, pointing at the production Supabase project.eas build --platform ios --profile production (the first run signs in to Apple and makes the certificate and provisioning profile).eas submit --platform ios --latest uploads it to App Store Connect for TestFlight and review.The production Supabase project needs the same backend steps as above (supabase db push, supabase functions deploy delete-account, the Apple and Google providers). Its Apple client ids should be app.airstrive.mobile only: host.exp.Exponent is for Expo Go and doesn't belong in production.
app.json declares the privacy manifest (ios.privacyManifests), that the app uses no non-exempt encryption, and light and dark app icons. Settings links to the privacy policy and help pages on the website (website/privacy.html, website/support.html), which are also the URLs App Store Connect asks for.
Four widgets, written in SwiftUI in targets/widget/ and added to the Xcode project by @bacons/apple-targets on prebuild: Next up (small and medium, a different open square each hour), Lately (small and medium, your newest photos as polaroids), Your grids (medium, four grids at a glance) and Scrapbook (large: Lately, Next up and what friends shared). Tapping one opens that square, moment or the shelf.
The app writes widget.json and small JPEGs of recent photos to the App Group group.app.airstrive.mobile after every edit or sync pull and when it comes to the front (src/widgets/), and asks iOS to redraw. Widgets need a development or store build (eas build), not Expo Go. EAS makes the widget's App ID (app.airstrive.mobile.widget), the App Group and its provisioning profile on the first build. To build in Xcode instead, set ios.appleTeamId in app.json and run npx expo prebuild -p ios --clean.
The website is the landing page and privacy page in website/, plus the same app built for the browser at /app. On the web, photos live in IndexedDB and SQLite runs as WebAssembly, so it works offline like the phone app and syncs through the same Supabase account. On the web you sign in with Google or with a code sent by email. Sign in with Apple on the web needs a paid Apple Developer account.
npm run build:site # writes dist-site/: website/ at the root, the app under /app
vercel.json deploys it on Vercel's free plan:
vercel.json.EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY.https://<your-domain>/app/** (and http://localhost:8081/** for development) to the redirect URLs.https://<project-ref>.supabase.co/auth/v1/callback as an authorized redirect URI.https://<your-domain>/auth as an authorized redirect URI on the same Google client (website/auth.html hands the answer to the app; add http://localhost:8081/ too for development), and set EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID in Vercel. Without that variable the site signs in through Supabase as in step 4.{{ .Token }} to the Magic Link template. Supabase's built-in mailer only reaches the project's team members, so set up custom SMTP before anyone else signs in by email.Vercel Web Analytics counts page views on the website and the web app (cookieless; turn it on in the Vercel project's Analytics tab). The phone app has none.
On a computer screen (at least 960 px wide) a few screens switch to wide layouts from src/wide/. The grid page shows the board beside a panel for the selected square, with hover previews and arrow keys. Mosaic mode shows the whole grid on a dark wall. Memory Lane pages open as a two-page book. Every other screen stays in a phone-width column. useWideLayout() in src/ui/wide.web.ts decides which layout to use, and it is always false on phones.
The app pages need Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (expo-sqlite uses SharedArrayBuffer). vercel.json sends them for /app, and metro.config.js does the same for npx expo start --web.
npm run typecheck # app and tests
npm test # repo rules and two-phone sync, on Node 22's built-in SQLite
npm run test:db # migrations, RLS and last-write-wins on a throwaway Postgres (PGHOST/PGPORT)
app/ screens (expo-router)
welcome.tsx, username.tsx sign-in and picking a name
(tabs)/index.tsx home shelf: greeting, things to try, your grids
(tabs)/lane.tsx Memory Lane index, moments and chapter covers
onboarding/ interests, then suggested grids to add
(tabs)/atlas.tsx the Atlas: search, this season, rows by theme
atlas/[gridId].tsx grid preview: add, copy and edit, report
make.tsx make your own, with Suggest the rest
report/[gridId].tsx report a shared grid
grid/[id].tsx the board, tiles, bottom sheet, freebies
moment/[squareId].tsx log or edit a moment (photos, journal, date, chips)
lane/[momentId].tsx scrapbook page with polaroids and handwriting
lane/chapter/[userGridId] chapter cover: the finished mosaic, print as poster
settings.tsx backup status, export, blocked people, sign out, delete account
(tabs)/profile.tsx Profile: bio, friends and requests, grids you share
friends/ add a friend (username search), my QR code and link, scan a code
f/[code].tsx where a friend link or scanned code lands, signed in or not
people/[userId].tsx someone's profile: add, accept, remove, block, report
people/grid/[userGridId] someone's grid, read-only, with the moments shared with you
together/start.tsx turns a grid into a group grid (needs a connection)
together/[groupId].tsx who's in a group grid: invite friends, cancel invites, leave
report/moment, report/user report a moment or a person
src/
data/ the six starter grids (424 prompts), sizes, placement, seeding, adopting
atlas/ themes and interests, picks, the Atlas catalog, the preview
suggest/ the reviewed prompt bank and Suggest the rest
friends/ audiences, the friends and profile calls, QR codes, links
groups/ group grids: each person's part of a square, invites, leaving
db/schema.ts SQLite schema and upgrades (v0 phones upgrade in place)
db/repo/ reads and writes by area: shelf, board, moments, lane, grids
db/queue.ts the sync queue: one entry per changed row
sync/engine.ts push, pull, photo upload and download, conflict rules
sync/tables.ts the columns each synced table sends and receives
db/database.types.ts generated types for the Supabase tables and functions (npm run gen:types)
sync/SyncProvider.tsx when to sync: launch, foreground, network back, after writes
auth/AuthProvider.tsx Apple / Google / email sign-in, username, sign-out, deletion
lib/exportData.ts the export zip: moments.json, photos/, memory-lane.pdf
lib/printables.ts HTML for the poster and the scrapbook PDF
widgets/ what the home screen widgets show, written for them to read
lib/*.web.ts browser versions: photos in IndexedDB, printing and downloads, dialogs
targets/widget/ the iOS home screen widgets (SwiftUI), with the app's fonts
website/ landing page, privacy page and the /f/<code> friend link page (static HTML)
supabase/
migrations/ tables, row-level security, triggers, starter grid seed
functions/delete-account deletes photos, rows and the auth user; keeps what was shared to group grids
admin/ Next.js admin site: reports, hide grids, suspend sharing
tests/ SQL checks for RLS and sync rules
test/ node --test suites and a fake server
scripts/starter-sql.ts regenerates the starter grid seed from src/data
can_view() in Postgres, and photo files are readable only while their moment is.user_grids row whose group_id is its own id; every row under it carries the same group_id, set by Postgres from the parent. Membership alone decides who reads it: the owner rules cover solo rows only, so someone who leaves loses access to what they added too, and it stays for the others.body is always empty; the words are in moment_entries.user_id on what it added to group grids (shown as "a former member"), deletes its Private entries and photos there, and moves its shared photo files to groups/{group_id}/.sync_queue in the same transaction. The pusher sends the row as it is at push time, parents before children, a batch per table, and retries with backoff (1 s, 10 s, 1 min, 10 min, then hourly). After seven days of failures a change is marked as couldn't sync, and Settings offers Try again.server_updated_at, starting two minutes before the last cursor.updated_at wins per row; a deleted row stays deleted; a row with queued local changes is never overwritten by a pull. These rules hold on both the phone and in Postgres triggers.my_groups() which groups the person is in. A group just joined is fetched whole (its rows are older than the pull cursors); one just left is removed from the phone. group_unshares tells phones to drop an entry or photo someone made Private.save_grid() before any other rows, one call per grid. A grid that won't save is retried without holding up the rest. Sharing while sharing is suspended saves the grid as private.photos bucket at {user_id}/{photo_id}.webp, and other phones download them on the next sync. Photo paths are stored relative to the documents folder.