Skip to content

Latest commit

 

History

History
637 lines (464 loc) · 35.1 KB

File metadata and controls

637 lines (464 loc) · 35.1 KB

SnowLog 技術仕様書

最終更新: 2026-08-31 対象バージョン: v1.3.0 基準: main ブランチの現行実装


1. 文書の目的

本書は、SnowLog の現在の実装状態を共有するための技術仕様書である。 企画案や理想像ではなく、src/app / src/hooks / src/services / src/database に存在するコードを基準に、現時点での機能・責務・データ構造・プラットフォーム差分を整理する。


2. プロダクト概要

  • 目的: スキー・スノーボード動画をローカルで整理し、あとから素早く振り返れる個人用ログアプリを提供する。
  • 主な利用者: 撮影本数が多く、クラウド共有よりも「整理しやすさ」「練習ログ化」「あとからの見返しやすさ」を重視する個人スキーヤー / スノーボーダー。
  • 中核価値:
    • 動画にゲレンデ名・タイトル・メモ・タグ・滑走種別を付けて残せる
    • ホーム / カレンダー / 検索 / 統計から違う切り口で振り返れる
    • お気に入りや日記によって、単なる保存ではなく「思い出と練習記録の両方」を蓄積できる
  • データ方針: ユーザーデータはローカル SQLite を中心に保持し、動画の一覧・属性・日記・設定を端末内で管理する。

3. 対応プラットフォーム

プラットフォーム 状態 補足
iOS 主対象 / 機能が最も充実 フォトライブラリ連携、iCloud アセット取得、写真アプリ導線あり
Android 未対応 / 将来検討 一部の下地はあるが、配布・動作保証・UI 検証の対象外
Web プレビュー用途 SQLite / MediaLibrary 非対応のため、主にモック・スタブで画面確認を行う

補足:

  • Web は機能等価な本番実装ではなく、UI プレビュー用途の簡易実装である。
  • iOS のネイティブ機能を使う画面では、Web 側にスタブが置かれている場合がある。
  • Android は将来的な対応候補だが、現時点のプロダクトとしては iOS のみをサポートする。

4. アプリ全体構成

4.1 ルート構成

  • ルートは Expo Router ベース。
  • src/app/_layout.tsx が起点となり、マイグレーション完了後に各画面を表示する。
  • メイン導線は src/app/(tabs) 配下の 5 タブ構成。
  • 個別画面として、動画インポート、動画詳細、各種設定画面を Stack で管理する。

4.2 タブ構成

タブ 役割 主な内容
index ホーム ゲレンデ別タイムライン、全件 / お気に入り切り替え、一括選択
dashboard 統計 シーズン単位の集計、ゲレンデ別ランキング、種別分布、月別推移
calendar カレンダー 月 / 週表示、日別動画一覧、日記表示・編集
search 検索 テキスト、ゲレンデ、タグ、期間プリセットによる絞り込み
settings 設定 カレンダー設定、滑走種別管理、お気に入りゲレンデ管理、タグ管理、重複候補確認、バックアップの書き出し/復元、不要ファイル削除

4.3 Stack で管理される主な画面

  • video-import
  • video/[id]
  • settings/calendar
  • settings/techniques
  • settings/favorite-resorts
  • settings/tags
  • settings/duplicate-candidates

5. 起動時処理

アプリ起動時には、単に画面を出すだけでなく以下の初期化処理を実行する。

  1. Drizzle migration を実行
  2. デフォルトの滑走種別候補を差分シード
  3. capturedAt が不正な動画レコードを修復
  4. サムネイル URI の相対パス移行(必要時のみ、後述)
  5. 参照されていないファイルの掃除(非ブロッキング。InteractionManager.runAfterInteractionscapturedAt 修復と並べて実行、後述)
  6. App Store 公開バージョンを参照した任意アップデート案内(後述)

i18n のロケールはこの流れには含まれない。src/i18n/index.ts のモジュール初期化時にデバイスロケールから一度だけ解決され、永続化設定は読まない(§14 参照)。

5.1 滑走種別候補のシード

  • DEFAULT_TECHNIQUE_OPTIONS を基準に、未登録の種別だけ追加する。
  • 既存ユーザーデータを壊さず、差分だけ補充する方式を採用している。

5.2 capturedAt 修復

  • 初回または修復バージョン更新時は全動画をスキャンする。
  • 以後は、明らかに不正なタイムスタンプだけを高速チェックする。
  • 修復時は MediaLibrary の creationTime を参照し、必要なら DB を更新する。
  • iCloud 専用アセットやメタデータ取得失敗は、起動を止めずにスキップする。

5.3 サムネイル URI の相対パス移行

  • v1.1.x 以前のレコードはサムネイル URI を 絶対パス(iOS のアプリコンテナ UUID 込み)で保持していた。
  • iOS はアプリ更新時にコンテナを再配置することがあり、絶対パスは突然無効化される。
  • 本マイグレーションは app_preferencesthumbnail_migration_version で初回のみガードして実行する。
  • 各動画について、(1) 相対パス(thumbnails/<id>.jpg)への正規化、(2) ファイル存在確認、(3) 不存在時はソース動画から再生成、(4) ソース動画も到達不能な場合は永続的欠損サムネイル用センチネル THUMBNAIL_MISSING_SENTINEL を保存、の順で処理する。
  • 進捗は ThumbnailMigrationScreen がブロッキング UI として表示する。
  • 1 件のエラーで全体を中断しない設計とし、失敗行は次回起動時に再試行されないよう移行バージョンを更新する。

5.4 i18n 初期ロケール解決

  • 永続化された言語設定は読まない。 アプリ内の言語ピッカーは bab0b45 で削除済みで、 app_locale の行も起動時に消される。
  • src/i18n/index.ts のモジュール初期化時に expo-localizationgetLocales()[0]?.languageCode を 一度だけ読み、"ja" ならそのまま、それ以外は "en" へ正規化して i18n-js に固定する。
  • 実行時の切り替え機構は持たない。ユーザーは iOS の言語設定を変える(=アプリが再起動する)。

5.5 任意アップデート案内

  • 起動後、UI 表示を妨げないよう InteractionManager.runAfterInteractions() の中で更新確認を実行する。
  • src/services/updateCheckService.ts が App Store Lookup API (https://itunes.apple.com/lookup?id=6761445679&country=jp) を取得し、返却された version と現在の expo.version を比較する。
  • remote version が現在の version より新しい場合のみ、Alert で任意アップデートを案内する。
  • 案内には「あとで」と「App Storeで見る」を表示し、trackViewUrl が返ればそれを開く。取得できない場合は固定の App Store URL にフォールバックする。
  • 同じ App Store version に対して「あとで」または App Store 遷移を選んだ場合、app_preferencesdismissed_update_prompt_version に保存し、同一 version では再表示しない。
  • オフライン、timeout、App Store 側の一時エラー、レスポンス不正は全て無視し、アプリ起動や通常利用をブロックしない。
  • 最新 version の正は App Store metadata とし、pr/websnowlog.kmchan.jp に更新確認用 JSON は配置しない。

6. データモデル

6.1 中心テーブル

export const videos = sqliteTable("videos", {
    id: text("id").primaryKey(),
    assetId: text("asset_id").notNull().unique(),
    filename: text("filename").notNull(),
    thumbnailUri: text("thumbnail_uri").notNull(),
    duration: int("duration").notNull().default(0),
    capturedAt: int("captured_at").notNull(),
    skiResortName: text("ski_resort_name"),
    memo: text("memo").notNull().default(""),
    title: text("title"),
    techniques: text("techniques"),
    isFileAvailable: int("is_file_available").notNull().default(1),
    isFavorite: int("is_favorite").notNull().default(0),
    createdAt: int("created_at").notNull(),
    updatedAt: int("updated_at").notNull(),
});

6.2 テーブル一覧

テーブル 役割 備考
videos 動画メタデータ本体 techniques は JSON 文字列、isFavorite を保持
tags タグマスタ 種別は technique / skier / custom
video_tags 動画とタグの中間テーブル 多対多を表現
technique_options 滑走種別の選択肢 ユーザー追加・削除可
favorite_resorts お気に入りゲレンデ 入力補完に利用
app_preferences 汎用 key-value 設定 現在は weekStartDay など
diary_entries 日記エントリー 1 日 1 レコード、dateKey 一意

6.3 重要な制約

  • videos.assetId はユニーク
  • tags(name, type) の組み合わせでユニーク(migration 0007_simple_molecule_man.sql で導入)
  • diary_entries.dateKey はユニーク
  • SQLite 接続は foreign_keys = ON および WAL モードで開く。タグ更新(setTagsForVideo)は単一トランザクション内で行うため、中間テーブルの整合性が保たれる。

6.4 エンティティ関係

videos --< video_tags >-- tags
   |
   +-- technique_options   (選択候補)
   +-- favorite_resorts    (入力補助)
   +-- app_preferences     (表示設定)
   +-- diary_entries       (日別ログ)

6.5 型上の補足

  • UI 層では VideoWithTags を主に扱う。
  • videos.techniques は DB では JSON 文字列だが、フック / 表示側では string[] | null として扱う。
  • FilterOptionsskiResortName / tagIds / dateFrom / dateTo / searchText / favoritesOnly を持つ。

7. 主要ユーザーフロー

7.1 ホーム (index)

  • 動画一覧をゲレンデ別セクションにまとめて表示する。
  • セグメント切り替えで「すべて」と「お気に入り」を往復できる。
  • 並び順ピッカー から newest(撮影日の新しい順)/ oldest(古い順)/ resort(ゲレンデ名でグルーピング)を切り替えられる。選択は app_preferenceshome_sort_order に永続化する。
  • 動画カードのスワイプ削除に対応する。確認アラート経由で削除を確定する。
  • 動画カードの長押しで一括選択モードに入り、以下をまとめて実行できる。
    • お気に入り ON / OFF
    • 複数動画の削除
  • 削除時は DB レコードだけでなく、サムネイルや managed 動画ファイルもクリーンアップする。
  • インポート完了後の復帰時には、bulkImportSummaryService に積まれた一括インポートサマリー(成功 / スキップ / 失敗件数)をアラートで提示する。

7.2 動画インポート (video-import)

単体インポート

  1. フォトライブラリ権限を要求
  2. 1 本の動画を選択
  3. 既存 assetId と照合して重複インポートを防止
  4. EXIF / MediaLibrary から撮影日時と GPS を解決
  5. 一時ステージングした URI を使ってサムネイル生成・保存
  6. タイトル / ゲレンデ / メモ / タグ / 滑走種別を付けて保存

補足:

  • iCloud 上のアセットは shouldDownloadFromNetwork: true で取得を試みる。
  • assetId が返らない場合は synthetic: プレフィックス付き ID を発行する。

一括インポート

  • 一度に最大 20 本 まで選択できる。
  • 既存の assetId と一致する動画は自動で skipped 扱いにする。
  • 各動画は順番にインポートされ、進捗 UI を表示する。
  • GPS が取れた動画は、近いゲレンデ候補ごとにグループ化して確認ダイアログを出す。
  • 確認後は対象グループに対して一括でゲレンデ名を反映する。
  • ステージングファイルは各動画の処理後に即時削除する。
  • 完了サマリー: 全件処理が終わると bulkImportSummaryService.setPendingBulkImportSummary に成功 / スキップ / 失敗件数を積み、インポート画面を閉じる。ホーム画面はフォーカス時に consumePendingBulkImportSummary でこれを取り出し、アラートで提示する。
  • インポート画面はインポート進行中の意図せぬ離脱をブロックし、完了時にのみ自動で閉じる。

7.3 動画詳細 (video/[id])

  • expo-video を用いて動画再生を行う。
  • タイトル・メモは debounce 付きの自動保存。
  • ゲレンデ名・滑走種別・タグは画面から編集できる。
  • お気に入りは即時トグル可能。
  • 削除時は動画レコード、サムネイル、managed 動画ファイルを整理する。
  • 元アセットが消えている場合は isFileAvailable を使って再生不可状態を表示する。
  • iOS では、条件を満たす場合のみ写真アプリへ戻る導線を表示する。

7.4 カレンダー (calendar)

  • 月表示 / 週表示を切り替えられる。
  • weekStartDay 設定に応じて、週の開始曜日を月曜 / 日曜で変更できる。
  • 各日には以下の集約情報を載せる。
    • 動画本数
    • 代表サムネイル
    • ゲレンデ色ドット
    • 日記の有無
  • 日付選択時は、その日の動画一覧と日記カードを下部に表示する。
  • 日記は DiaryEditModal で作成 / 更新 / 削除できる。

日記で保持する主な項目

  • skiResortName
  • weather
  • snowCondition
  • impressions
  • temperature
  • companions
  • fatigueLevel
  • expenses
  • numberOfRuns

7.5 検索 (search)

  • FilterBar で以下の条件を組み合わせて絞り込みできる。
    • テキスト検索
    • ゲレンデ名
    • タグ
    • 期間プリセット(今月 / 先月 / 今シーズン)
  • テキスト検索は UI 上はタイトル・メモ中心の導線だが、実装上はファイル名も検索対象に含む。
  • フィルタ結果は件数付きで一覧表示する。
  • ダッシュボードから検索画面へドリルダウンする用途もある。

7.6 統計 (dashboard)

  • 集計単位はシーズンで、定義は 11 月〜翌年 5 月
  • シーズン切り替え UI により、過去シーズンを選択できる。
  • 主な表示内容:
    • summary(滑走日数、動画数、総再生時間、ゲレンデ数、お気に入り数)
    • resortRanking(訪問日数・動画数・最終訪問日)
    • techniqueDistribution(滑走種別の分布)
    • monthlyTrend(月別の動画数 / 滑走日数)
    • heatmapDays(日別密度)
    • recentVideos(最近の動画)
  • 一部カードから検索画面へ条件付き遷移できる。

7.7 設定 (settings)

設定画面は、他画面へ遷移する 5 項目と、その場で実行する保守行 1 つで構成する。

遷移する 5 項目:

  1. カレンダー設定
  2. 滑走種別の管理
  3. お気に入りスキー場
  4. タグの管理
  5. 重複候補の確認

保守行:

  • 不要ファイルを削除

カレンダー設定

  • 週の開始曜日を monday / sunday で保存する。

滑走種別の管理

  • technique_options に対する追加・削除を行う。
  • 既存動画に設定済みの値は削除しても自動書き換えしない。

お気に入りスキー場

  • SkiResortSearch で候補検索し、入力補完に使うゲレンデを保存する。

タグの管理

  • 現在は主に custom タグの追加・削除を行う。
  • 削除時は中間テーブル video_tags からも関連付けを除去する。

重複候補の確認

  • 似ている動画を自動検出し、グループ単位で確認できる。
  • ここから候補動画の削除を実行できる。

不要ファイルを削除(保守行)

  • 画面遷移しない。タップすると確認 → 実行 → 結果の 3 段階をアラートで進める。
  • 確認時に hapticWarning、成功時に hapticSuccess、失敗時に hapticError を鳴らす。
  • 結果アラートは削除件数を示し、0 件のときは専用の文言に切り替える。
  • 実行中は多重起動を防ぐため、同じ行の再タップを無視する。
  • 実処理は orphanedFileCleanupService.cleanupOrphanedFiles()。起動時にも同じ関数が非ブロッキングで走る(§5)。

8. 重複候補判定ロジック

重複候補は、以下の情報を組み合わせてスコアリングする。

  • 動画長の差
  • 撮影時刻の差
  • 正規化したファイル名の一致 / 類似
  • ゲレンデ名の一致

代表的な判定条件:

  • ファイル名がほぼ一致し、長さの差が 2 秒以内
  • 撮影時刻の差が 5 秒以内で、長さの差が 1 秒以内
  • 総合スコアが一定以上

検出後は、ペア単位ではなく連結グラフとしてグループ化し、high / medium の信頼度を付与する。


9. リポジトリ / フック / サービスの責務分担

9.1 Repository

ファイル 主責務
Repository videoRepository.ts 動画 CRUD、絞り込み、favorite 切替、一括 favorite、削除
Repository tagRepository.ts タグ CRUD、動画との関連付け、カスタムタグ削除
Repository favoriteResortRepository.ts お気に入りゲレンデの管理
Repository techniqueOptionRepository.ts 滑走種別候補の管理
Repository dashboardRepository.ts シーズン統計の集計
Repository diaryEntryRepository.ts 日記の取得、範囲検索、upsert、削除
Repository appPreferenceRepository.ts key-value 設定の保存

9.2 Hook

Hook 主責務
useVideos フィルタ条件に応じた動画一覧取得
useVideoDetail 動画詳細の取得、更新、削除、ファイル存在確認
useCalendarEnhanced 月 / 週表示、DayInfo 集約、日記存在の統合
useDiaryEntry 日記 1 件の読み書き
useDashboard シーズン選択と統計取得
useAppPreference 設定値の読み書き
useSelectionMode 一括選択モードの状態管理
useTranslation i18n の購読フック。useSyncExternalStore で locale 変更を全購読画面に再描画させ、{ t, locale, preference, setPreference } を返す

9.3 Service

Service 主責務
mediaService.ts MediaLibrary 権限、アセット取得、iCloud ダウンロード付き再取得
importService.ts インポート保存、サムネイル生成、タグ反映
managedVideoFileService.ts synthetic 動画のアプリ内保存
thumbnailService.ts サムネイル生成 / 削除
duplicateDetectionService.ts 重複候補のスコアリングとグルーピング
videoDeletionService.ts 動画削除時のクリーンアップ統合
exportService.ts JSON バックアップ生成と共有
bulkImportSummaryService.ts 一括インポートの結果(成功 / スキップ / 失敗件数)をプロセス内で揮発キャッシュし、ホームへ引き渡す
hapticsService.ts expo-haptics の薄いラッパ。UnavailabilityError を吸収する safeFire 経由で hapticLight / Medium / Selection / Success / Warning / Error を提供する
thumbnailMigrationService.ts 旧形式(絶対パス)のサムネイル URI を相対パスへ正規化し、欠損時は再生成または欠損センチネルでマークする一度限りの起動マイグレーション
orphanedFileCleanupService.ts documentDirectory 配下の thumbnails/videos/ を走査し、videos テーブルから参照されていないファイルを削除する。起動時(非ブロッキング)と設定画面の保守行から呼ばれる
updateCheckService.ts App Store Lookup API で公開バージョンを取得し、任意アップデート案内の要否を判定する。見送ったバージョンは dismissed_update_prompt_version に記録する

10. メディア保存ポリシー

10.1 通常のフォトライブラリアセット

  • 基本的には assetId を保持し、元動画は MediaLibrary 側を参照する。
  • サムネイルのみアプリ管理領域へ保存する。

10.2 Synthetic import

  • picker が assetId を返さない場合、synthetic: 付き ID を採番する。
  • この場合は元 URI をそのまま参照せず、documentDirectory/videos/ 配下へコピーして保持する。
  • 将来の Android 対応を見据え、managed 保存側では content:// URI も扱えるようにしている。ただし、現時点では Android をサポート対象に含めない。

10.3 サムネイル

  • サムネイルは documentDirectory/thumbnails/ 配下へ JPEG として保存する。
  • 動画削除時にはサムネイルも削除する。

10.4 ファイル欠損時の扱い

  • 元アセットが消えた、または managed 動画が見つからない場合は isFileAvailable を使って再生不可を表現する。
  • データは残しつつ、再生 UI 側で利用不能を明示する。

10.5 掃除の不変条件

documentDirectory 配下でアプリが保持し続けるのは、現在の videos 行から参照されているファイルだけである。

  • thumbnails/videos.thumbnailUri(相対パス)が指すファイル
  • videos/videos.managed_video_path が指すファイル(managedPathToUri で現在のコンテナ基準に解決する)

videos/ の所有権を決めるのは videos.storage_modecopy の行が持つ managed_video_path だけである。 assetId の形は見ない(#86)。現状 copy になるのは取り込み時に assetId が synthetic だった行だけなので 結果は従来と同じだが、判定の根拠が列に移っている。

パスを持たない copy 行(移行時に ID がファイル名として使えなかったもの)は何も所有せず、再生では 「見つからない」として扱う。実体が残っていても消さない——参照されないファイルとして回収対象にはなるが、 5 分の猶予を越えるまで消えないため手動復旧の余地がある。

上記に該当しないものは回収対象で、orphanedFileCleanupService.cleanupOrphanedFiles() が削除する。実行契機は 2 つ。

  • 起動時。InteractionManager.runAfterInteractions の中で非ブロッキングに走り、失敗しても無視する
  • 設定画面の保守行「不要ファイルを削除」(§7.7)

ただし未参照でも即座には消さない。安全弁が 2 つある。

  • 更新から 5 分以内のファイルは残すDEFAULT_MINIMUM_FILE_AGE_MS)。DB へ書き込む前の生成直後のファイルを、掃除が追い越して消してしまうのを防ぐ
  • protectFilesFromOrphanedCleanup() で明示保護された URI は残す。インポート処理のように、参照が確定するまでの間だけファイルを守りたい経路が使う

したがって、動画レコードを消したあとにファイルの後始末が漏れても、いずれ回収される。逆に言えば、videos から参照されないファイルを長期的に残す設計はできない。恒久的に保持したいファイルを増やすなら、この掃除の対象ディレクトリの外に置くこと。


11. バックアップ / エクスポート

exportService.ts には、全データを JSON として書き出して共有シートを開く処理が実装されている。

11.1 エクスポート対象

  • videos
  • tags
  • techniqueOptions
  • favoriteResorts
  • diaryEntries
  • preferences

11.1.1 スキーマバージョン

ペイロードの先頭に schemaVersion が入る。現在は 2

  • v1: 保存方式を持たない。読み込み時に assetIdsynthetic: 始まりなら copy、 そうでなければ reference と推定する。当時はそれ以外にコピーを持つ手段が無かったので、 この推定は当時のデータについて正確である。
  • v2: 各動画に storageMode と相対パスの managedVideoPath が入る(#86)。

v1 しか読めない古いビルドは v2 のファイルを開けない。 これは意図した挙動で、保存方式を 落として読み込むと管理コピーの行がすべて参照方式として復元され、存在しない写真を指すことになる。 リリースノートにこの非互換を明記すること。

読み込み側は「方式とパスが矛盾する行」を修復せずスキップする。参照なのにパスを持つ、コピーなのに 別の動画 ID のファイル名を指す、.. を含む、リモート URL——いずれも、どちらが正しいかを推測すると ある動画のバイトを別の動画のメタデータに結び付けかねない。

11.2 導線と処理の流れ

  • 設定画面(src/app/(tabs)/settings/index.tsx)の「エクスポート」行から実行する。
  • ペイロードの組み立ては純粋関数 buildExportPayload()src/services/exportPayload.ts)に分離されており、 scripts/tests/exportPayload.test.cjs で検証している。exportService.ts は I/O だけを持つ。
  • ファイルは cacheDirectorysnowlog-backup-YYYYMMDD-HHmm.json として書き出し、共有シートを開く。 共有後に削除はせず、次回エクスポート時に古いバックアップを掃除する。
  • 共有シートが利用できない場合は ExportError を投げる。この例外のメッセージだけがユーザーに提示される。

11.3 インポート(バックアップからの復元)

  • 設定画面の「バックアップから復元」行から実行する。ファイル選択 → 件数のプレビュー → 確認 → 書き込み。
  • 検証と正規化は純粋関数 parseExportPayload()src/services/importPayload.ts)が担い、 scripts/tests/importPayload.test.cjs で検証している。importJsonService.ts は I/O だけを持つ。 なお importService.ts は名前が似ているが別物で、写真ライブラリからの動画取り込みである。
  • 既存行は上書きせずスキップする(insert-or-ignore)。同じファイルを二度取り込んでも結果は変わらない。
  • 同一性は自然キーで判定する。 タグは name + type、滑走種別は name、日記は dateKey。 バックアップ内の id はローカルの連番のため、別 DB では別物を指す。
  • 復元時に動画ごとにアセットの実在を確認し、isFileAvailable を実測値で入れる。 サムネイルはファイルが実在する場合のみパスを保ち、無ければ THUMBNAIL_MISSING_SENTINEL にする。
  • 設定は home_sort_orderweekStartDay のみ復元する(RESTORABLE_PREFERENCE_KEYS)。

11.4 現在の制限

  • バックアップにはメタデータしか入っていない。 動画ファイルもサムネイル画像も含まれない。 そのため別の端末で復元してもログは戻るが動画は再生できない。同一端末での再インストールなら完全に戻る。 v2 で managedVideoPath が入るようになったが、これはパスであってバイトではない。復元先に実体が 無ければ、その行は「アプリ内の動画が見つからない」状態で戻る。写真ライブラリ側に原本が残っていても copy 行は参照方式へ降格しない——指しているのはアプリのコピーであって原本ではないため。
  • 動画の再リンク(新しい端末の写真ライブラリと突き合わせて assetId を張り直す)は未実装。 これが入るまで、本機能を「機種変更対応」と説明してはいけない。
  • エクスポート・インポートとも実機での実行実績がまだ無いexpo-document-picker はネイティブモジュールのため dev client の再ビルドが要る。詳細は .memory/doc-drift.md
  • applyImportPlan() はアトミックではない。6 テーブルに順に書くため途中失敗で一部だけ入る。 全書き込みが insert-or-ignore なので、やり直しが安全であることが緩和策になっている。

12. プラットフォーム差分メモ

12.1 iOS

  • 主なターゲットプラットフォーム。
  • iCloud 上のアセット取得に対応。
  • 詳細画面では、条件付きで写真アプリへ戻るリンクを表示する。

12.2 Android

  • android.package を設定済み。
  • content:// URI を含むインポート経路など、一部の実装下地は存在する。
  • ただし、Google Play 配布・実機検証・UI 調整は未実施であり、現時点のサポート対象ではない。
  • iOS ユーザーが増え、プロダクトとして十分に成長した段階で Android 版を検討する。

12.3 Web

  • SQLite / MediaLibrary / Sharing の制約上、本番相当の機能提供はしない。
  • *.web.ts / *.web.tsx ではモックやアラートベースの代替実装を用いる。
  • 目的は、主にレイアウト確認と簡易 UI プレビューである。

13. 現在の仕様として重要なポイント

古い仕様書との差分として、特に以下は現行実装で重要である。

  1. タブは 4 つではなく 5 タブdashboard を含む)
  2. 日記機能 diary_entries が存在する
  3. 動画は isFavorite を持つ
  4. 一括選択・一括お気に入り・一括削除がホームにある
  5. 重複候補確認画面が設定にある
  6. Android は未対応だが、将来対応を見据えた一部の下地がある
  7. JSON バックアップ基盤が実装済みである
  8. 日英 2 言語の i18n 基盤が導入済みで、端末の言語設定に追従する(アプリ内に切替 UI は無い)
  9. ハプティクスが主要操作(一括選択開始、お気に入りトグル、削除確定、インポート完了など)に配線済み
  10. サムネイルは documentDirectory 相対パスで保存され、iOS のコンテナ再配置に耐性がある
  11. ホームに並び順切替スワイプ削除が組み込まれている
  12. 一括インポートの結果はホームへの復帰時にサマリーアラートとして提示される

14. 国際化 (i18n)

14.1 構成

src/i18n/ 配下にエンジンを集約する。

ファイル 役割
index.ts i18n-js インスタンス、モジュール初期化時のロケール解決、t / getCurrentLocale
useTranslation.ts { t, locale } を返す hook。ロケールは実行中に変わらないため購読機構は持たない
types.ts SupportedLocale = "ja" | "en"SUPPORTED_LOCALES
locales/ja.ts 日本語翻訳テーブル。フラットキー + ドメイン名前空間(home.*import.*diary.* など)
locales/en.ts 英語翻訳テーブル。Translations = typeof ja で型を強制し、キー欠落をコンパイル時に検出する

14.2 ロケールの決定

  • src/i18n/index.ts がモジュール初期化時に expo-localizationgetLocales()[0]?.languageCode一度だけ 読む。
  • ja 以外はすべて en に正規化し、その値を i18n-js に固定する。
  • 永続化は行わない。app_preferences に言語のキーは存在しない。

14.3 アプリ内に言語切替は無い

  • 言語ピッカーはコミット bab0b45 で削除済み。切替 API も setter も存在しない。
  • ユーザーは iOS の設定(設定 → SnowLog → 言語)で切り替える。アプリはそこで再起動されるため、実行中にロケールが変わることはない。
  • したがって useTranslation は再レンダー用の購読機構を持たず、{ t, locale } だけを返す。

14.4 iOS InfoPlist のローカライズ

  • 写真ライブラリの NSPhotoLibraryUsageDescription などのパーミッション文言は、リポジトリ直下の locales/ja.json および locales/en.json で言語別に定義する。
  • app.jsonexpo.locales ブロックがこれを iOS のリソースバンドルに配線する。
  • app.jsoninfoPlist 直下に書かれた値は 日本語のフォールバック値 として残しているのみで、ローカライズの一次ソースではない。
  • CFBundleLocalizations: ["ja", "en"]infoPlist に追加してあり、App Store 上で英語ストア向けにフォールバック表示される。

14.5 範囲外(既知の未対応)

  • ゲレンデ名(src/constants/skiResorts.json、378 件)と都道府県名は日本語固定。
  • ユーザーが設定画面で編集した滑走種別・タグ名・お気に入りゲレンデ名は、UI 言語に関係なく入力時の言語のまま表示する。

15. ハプティクス

15.1 設計方針

expo-haptics の native module は、新依存を取り込んだ後の dev client に古いバイナリが残っていると UnavailabilityError を投げる。これがアプリクラッシュへつながらないよう、hapticsService.ts では safeFire ラッパを介してすべての呼び出しを行い、同期 throw / Promise reject の双方を吸収する。

15.2 公開 API

関数 用途
hapticLight() お気に入りトグル等の軽い肯定アクション
hapticMedium() 長押しで一括選択モードに入る等のモード遷移
hapticSelection() ピッカー / セグメント切替の確定
hapticSuccess() バルクインポート完了等、複数ステップの成功
hapticWarning() 削除確定など、影響度の大きい操作の確定
hapticError() 操作の失敗通知

15.3 配線箇所

主要操作にのみ配線する方針で、リスト項目の単純タップやスクロール等のジェスチャには付与しない。具体的には、お気に入り切替・一括選択開始・削除確定・インポート完了・ピッカー切替等を対象としている。


16. 今後この文書を更新すべきタイミング

以下の変更が入った場合は、本書も更新対象とする。

  • タブ構成の変更
  • DB スキーマ変更
  • インポート / 削除 / バックアップ仕様の変更
  • 新しい設定画面の追加
  • プラットフォーム対応範囲の変更
  • ダッシュボード集計項目の変更

本書は README より技術寄り、コードコメントより俯瞰的、実装よりも先回りしすぎないレベルで維持するのが望ましい。