最終更新: 2026-08-31
対象バージョン: v1.3.0
基準: main ブランチの現行実装
本書は、SnowLog の現在の実装状態を共有するための技術仕様書である。
企画案や理想像ではなく、src/app / src/hooks / src/services / src/database に存在するコードを基準に、現時点での機能・責務・データ構造・プラットフォーム差分を整理する。
- 目的: スキー・スノーボード動画をローカルで整理し、あとから素早く振り返れる個人用ログアプリを提供する。
- 主な利用者: 撮影本数が多く、クラウド共有よりも「整理しやすさ」「練習ログ化」「あとからの見返しやすさ」を重視する個人スキーヤー / スノーボーダー。
- 中核価値:
- 動画にゲレンデ名・タイトル・メモ・タグ・滑走種別を付けて残せる
- ホーム / カレンダー / 検索 / 統計から違う切り口で振り返れる
- お気に入りや日記によって、単なる保存ではなく「思い出と練習記録の両方」を蓄積できる
- データ方針: ユーザーデータはローカル SQLite を中心に保持し、動画の一覧・属性・日記・設定を端末内で管理する。
| プラットフォーム | 状態 | 補足 |
|---|---|---|
| iOS | 主対象 / 機能が最も充実 | フォトライブラリ連携、iCloud アセット取得、写真アプリ導線あり |
| Android | 未対応 / 将来検討 | 一部の下地はあるが、配布・動作保証・UI 検証の対象外 |
| Web | プレビュー用途 | SQLite / MediaLibrary 非対応のため、主にモック・スタブで画面確認を行う |
補足:
- Web は機能等価な本番実装ではなく、UI プレビュー用途の簡易実装である。
- iOS のネイティブ機能を使う画面では、Web 側にスタブが置かれている場合がある。
- Android は将来的な対応候補だが、現時点のプロダクトとしては iOS のみをサポートする。
- ルートは Expo Router ベース。
src/app/_layout.tsxが起点となり、マイグレーション完了後に各画面を表示する。- メイン導線は
src/app/(tabs)配下の 5 タブ構成。 - 個別画面として、動画インポート、動画詳細、各種設定画面を Stack で管理する。
| タブ | 役割 | 主な内容 |
|---|---|---|
index |
ホーム | ゲレンデ別タイムライン、全件 / お気に入り切り替え、一括選択 |
dashboard |
統計 | シーズン単位の集計、ゲレンデ別ランキング、種別分布、月別推移 |
calendar |
カレンダー | 月 / 週表示、日別動画一覧、日記表示・編集 |
search |
検索 | テキスト、ゲレンデ、タグ、期間プリセットによる絞り込み |
settings |
設定 | カレンダー設定、滑走種別管理、お気に入りゲレンデ管理、タグ管理、重複候補確認、バックアップの書き出し/復元、不要ファイル削除 |
video-importvideo/[id]settings/calendarsettings/techniquessettings/favorite-resortssettings/tagssettings/duplicate-candidates
アプリ起動時には、単に画面を出すだけでなく以下の初期化処理を実行する。
- Drizzle migration を実行
- デフォルトの滑走種別候補を差分シード
capturedAtが不正な動画レコードを修復- サムネイル URI の相対パス移行(必要時のみ、後述)
- 参照されていないファイルの掃除(非ブロッキング。
InteractionManager.runAfterInteractionsでcapturedAt修復と並べて実行、後述) - App Store 公開バージョンを参照した任意アップデート案内(後述)
i18n のロケールはこの流れには含まれない。src/i18n/index.ts のモジュール初期化時にデバイスロケールから一度だけ解決され、永続化設定は読まない(§14 参照)。
DEFAULT_TECHNIQUE_OPTIONSを基準に、未登録の種別だけ追加する。- 既存ユーザーデータを壊さず、差分だけ補充する方式を採用している。
- 初回または修復バージョン更新時は全動画をスキャンする。
- 以後は、明らかに不正なタイムスタンプだけを高速チェックする。
- 修復時は MediaLibrary の
creationTimeを参照し、必要なら DB を更新する。 - iCloud 専用アセットやメタデータ取得失敗は、起動を止めずにスキップする。
- v1.1.x 以前のレコードはサムネイル URI を 絶対パス(iOS のアプリコンテナ UUID 込み)で保持していた。
- iOS はアプリ更新時にコンテナを再配置することがあり、絶対パスは突然無効化される。
- 本マイグレーションは
app_preferencesのthumbnail_migration_versionで初回のみガードして実行する。 - 各動画について、(1) 相対パス(
thumbnails/<id>.jpg)への正規化、(2) ファイル存在確認、(3) 不存在時はソース動画から再生成、(4) ソース動画も到達不能な場合は永続的欠損サムネイル用センチネルTHUMBNAIL_MISSING_SENTINELを保存、の順で処理する。 - 進捗は
ThumbnailMigrationScreenがブロッキング UI として表示する。 - 1 件のエラーで全体を中断しない設計とし、失敗行は次回起動時に再試行されないよう移行バージョンを更新する。
- 永続化された言語設定は読まない。 アプリ内の言語ピッカーは
bab0b45で削除済みで、app_localeの行も起動時に消される。 src/i18n/index.tsのモジュール初期化時にexpo-localizationのgetLocales()[0]?.languageCodeを 一度だけ読み、"ja"ならそのまま、それ以外は"en"へ正規化してi18n-jsに固定する。- 実行時の切り替え機構は持たない。ユーザーは iOS の言語設定を変える(=アプリが再起動する)。
- 起動後、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_preferencesのdismissed_update_prompt_versionに保存し、同一 version では再表示しない。 - オフライン、timeout、App Store 側の一時エラー、レスポンス不正は全て無視し、アプリ起動や通常利用をブロックしない。
- 最新 version の正は App Store metadata とし、
pr/webやsnowlog.kmchan.jpに更新確認用 JSON は配置しない。
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(),
});| テーブル | 役割 | 備考 |
|---|---|---|
videos |
動画メタデータ本体 | techniques は JSON 文字列、isFavorite を保持 |
tags |
タグマスタ | 種別は technique / skier / custom |
video_tags |
動画とタグの中間テーブル | 多対多を表現 |
technique_options |
滑走種別の選択肢 | ユーザー追加・削除可 |
favorite_resorts |
お気に入りゲレンデ | 入力補完に利用 |
app_preferences |
汎用 key-value 設定 | 現在は weekStartDay など |
diary_entries |
日記エントリー | 1 日 1 レコード、dateKey 一意 |
videos.assetIdはユニークtagsは(name, type)の組み合わせでユニーク(migration0007_simple_molecule_man.sqlで導入)diary_entries.dateKeyはユニーク- SQLite 接続は
foreign_keys = ONおよび WAL モードで開く。タグ更新(setTagsForVideo)は単一トランザクション内で行うため、中間テーブルの整合性が保たれる。
videos --< video_tags >-- tags
|
+-- technique_options (選択候補)
+-- favorite_resorts (入力補助)
+-- app_preferences (表示設定)
+-- diary_entries (日別ログ)
- UI 層では
VideoWithTagsを主に扱う。 videos.techniquesは DB では JSON 文字列だが、フック / 表示側ではstring[] | nullとして扱う。FilterOptionsはskiResortName/tagIds/dateFrom/dateTo/searchText/favoritesOnlyを持つ。
- 動画一覧をゲレンデ別セクションにまとめて表示する。
- セグメント切り替えで「すべて」と「お気に入り」を往復できる。
- 並び順ピッカー から
newest(撮影日の新しい順)/oldest(古い順)/resort(ゲレンデ名でグルーピング)を切り替えられる。選択はapp_preferencesのhome_sort_orderに永続化する。 - 動画カードのスワイプ削除に対応する。確認アラート経由で削除を確定する。
- 動画カードの長押しで一括選択モードに入り、以下をまとめて実行できる。
- お気に入り ON / OFF
- 複数動画の削除
- 削除時は DB レコードだけでなく、サムネイルや managed 動画ファイルもクリーンアップする。
- インポート完了後の復帰時には、
bulkImportSummaryServiceに積まれた一括インポートサマリー(成功 / スキップ / 失敗件数)をアラートで提示する。
- フォトライブラリ権限を要求
- 1 本の動画を選択
- 既存
assetIdと照合して重複インポートを防止 - EXIF / MediaLibrary から撮影日時と GPS を解決
- 一時ステージングした URI を使ってサムネイル生成・保存
- タイトル / ゲレンデ / メモ / タグ / 滑走種別を付けて保存
補足:
- iCloud 上のアセットは
shouldDownloadFromNetwork: trueで取得を試みる。 assetIdが返らない場合はsynthetic:プレフィックス付き ID を発行する。
- 一度に最大 20 本 まで選択できる。
- 既存の
assetIdと一致する動画は自動でskipped扱いにする。 - 各動画は順番にインポートされ、進捗 UI を表示する。
- GPS が取れた動画は、近いゲレンデ候補ごとにグループ化して確認ダイアログを出す。
- 確認後は対象グループに対して一括でゲレンデ名を反映する。
- ステージングファイルは各動画の処理後に即時削除する。
- 完了サマリー: 全件処理が終わると
bulkImportSummaryService.setPendingBulkImportSummaryに成功 / スキップ / 失敗件数を積み、インポート画面を閉じる。ホーム画面はフォーカス時にconsumePendingBulkImportSummaryでこれを取り出し、アラートで提示する。 - インポート画面はインポート進行中の意図せぬ離脱をブロックし、完了時にのみ自動で閉じる。
expo-videoを用いて動画再生を行う。- タイトル・メモは debounce 付きの自動保存。
- ゲレンデ名・滑走種別・タグは画面から編集できる。
- お気に入りは即時トグル可能。
- 削除時は動画レコード、サムネイル、managed 動画ファイルを整理する。
- 元アセットが消えている場合は
isFileAvailableを使って再生不可状態を表示する。 - iOS では、条件を満たす場合のみ写真アプリへ戻る導線を表示する。
- 月表示 / 週表示を切り替えられる。
weekStartDay設定に応じて、週の開始曜日を月曜 / 日曜で変更できる。- 各日には以下の集約情報を載せる。
- 動画本数
- 代表サムネイル
- ゲレンデ色ドット
- 日記の有無
- 日付選択時は、その日の動画一覧と日記カードを下部に表示する。
- 日記は
DiaryEditModalで作成 / 更新 / 削除できる。
skiResortNameweathersnowConditionimpressionstemperaturecompanionsfatigueLevelexpensesnumberOfRuns
FilterBarで以下の条件を組み合わせて絞り込みできる。- テキスト検索
- ゲレンデ名
- タグ
- 期間プリセット(今月 / 先月 / 今シーズン)
- テキスト検索は UI 上はタイトル・メモ中心の導線だが、実装上はファイル名も検索対象に含む。
- フィルタ結果は件数付きで一覧表示する。
- ダッシュボードから検索画面へドリルダウンする用途もある。
- 集計単位はシーズンで、定義は 11 月〜翌年 5 月。
- シーズン切り替え UI により、過去シーズンを選択できる。
- 主な表示内容:
summary(滑走日数、動画数、総再生時間、ゲレンデ数、お気に入り数)resortRanking(訪問日数・動画数・最終訪問日)techniqueDistribution(滑走種別の分布)monthlyTrend(月別の動画数 / 滑走日数)heatmapDays(日別密度)recentVideos(最近の動画)
- 一部カードから検索画面へ条件付き遷移できる。
設定画面は、他画面へ遷移する 5 項目と、その場で実行する保守行 1 つで構成する。
遷移する 5 項目:
- カレンダー設定
- 滑走種別の管理
- お気に入りスキー場
- タグの管理
- 重複候補の確認
保守行:
- 不要ファイルを削除
- 週の開始曜日を
monday/sundayで保存する。
technique_optionsに対する追加・削除を行う。- 既存動画に設定済みの値は削除しても自動書き換えしない。
SkiResortSearchで候補検索し、入力補完に使うゲレンデを保存する。
- 現在は主に
customタグの追加・削除を行う。 - 削除時は中間テーブル
video_tagsからも関連付けを除去する。
- 似ている動画を自動検出し、グループ単位で確認できる。
- ここから候補動画の削除を実行できる。
- 画面遷移しない。タップすると確認 → 実行 → 結果の 3 段階をアラートで進める。
- 確認時に
hapticWarning、成功時にhapticSuccess、失敗時にhapticErrorを鳴らす。 - 結果アラートは削除件数を示し、0 件のときは専用の文言に切り替える。
- 実行中は多重起動を防ぐため、同じ行の再タップを無視する。
- 実処理は
orphanedFileCleanupService.cleanupOrphanedFiles()。起動時にも同じ関数が非ブロッキングで走る(§5)。
重複候補は、以下の情報を組み合わせてスコアリングする。
- 動画長の差
- 撮影時刻の差
- 正規化したファイル名の一致 / 類似
- ゲレンデ名の一致
代表的な判定条件:
- ファイル名がほぼ一致し、長さの差が 2 秒以内
- 撮影時刻の差が 5 秒以内で、長さの差が 1 秒以内
- 総合スコアが一定以上
検出後は、ペア単位ではなく連結グラフとしてグループ化し、high / medium の信頼度を付与する。
| 層 | ファイル | 主責務 |
|---|---|---|
| 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 設定の保存 |
| Hook | 主責務 |
|---|---|
useVideos |
フィルタ条件に応じた動画一覧取得 |
useVideoDetail |
動画詳細の取得、更新、削除、ファイル存在確認 |
useCalendarEnhanced |
月 / 週表示、DayInfo 集約、日記存在の統合 |
useDiaryEntry |
日記 1 件の読み書き |
useDashboard |
シーズン選択と統計取得 |
useAppPreference |
設定値の読み書き |
useSelectionMode |
一括選択モードの状態管理 |
useTranslation |
i18n の購読フック。useSyncExternalStore で locale 変更を全購読画面に再描画させ、{ t, locale, preference, setPreference } を返す |
| 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 に記録する |
- 基本的には
assetIdを保持し、元動画は MediaLibrary 側を参照する。 - サムネイルのみアプリ管理領域へ保存する。
- picker が
assetIdを返さない場合、synthetic:付き ID を採番する。 - この場合は元 URI をそのまま参照せず、
documentDirectory/videos/配下へコピーして保持する。 - 将来の Android 対応を見据え、managed 保存側では
content://URI も扱えるようにしている。ただし、現時点では Android をサポート対象に含めない。
- サムネイルは
documentDirectory/thumbnails/配下へ JPEG として保存する。 - 動画削除時にはサムネイルも削除する。
- 元アセットが消えた、または managed 動画が見つからない場合は
isFileAvailableを使って再生不可を表現する。 - データは残しつつ、再生 UI 側で利用不能を明示する。
documentDirectory 配下でアプリが保持し続けるのは、現在の videos 行から参照されているファイルだけである。
thumbnails/—videos.thumbnailUri(相対パス)が指すファイルvideos/—videos.managed_video_pathが指すファイル(managedPathToUriで現在のコンテナ基準に解決する)
videos/ の所有権を決めるのは videos.storage_mode が copy の行が持つ 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 から参照されないファイルを長期的に残す設計はできない。恒久的に保持したいファイルを増やすなら、この掃除の対象ディレクトリの外に置くこと。
exportService.ts には、全データを JSON として書き出して共有シートを開く処理が実装されている。
videostagstechniqueOptionsfavoriteResortsdiaryEntriespreferences
ペイロードの先頭に schemaVersion が入る。現在は 2。
- v1: 保存方式を持たない。読み込み時に
assetIdがsynthetic:始まりならcopy、 そうでなければreferenceと推定する。当時はそれ以外にコピーを持つ手段が無かったので、 この推定は当時のデータについて正確である。 - v2: 各動画に
storageModeと相対パスのmanagedVideoPathが入る(#86)。
v1 しか読めない古いビルドは v2 のファイルを開けない。 これは意図した挙動で、保存方式を 落として読み込むと管理コピーの行がすべて参照方式として復元され、存在しない写真を指すことになる。 リリースノートにこの非互換を明記すること。
読み込み側は「方式とパスが矛盾する行」を修復せずスキップする。参照なのにパスを持つ、コピーなのに
別の動画 ID のファイル名を指す、.. を含む、リモート URL——いずれも、どちらが正しいかを推測すると
ある動画のバイトを別の動画のメタデータに結び付けかねない。
- 設定画面(
src/app/(tabs)/settings/index.tsx)の「エクスポート」行から実行する。 - ペイロードの組み立ては純粋関数
buildExportPayload()(src/services/exportPayload.ts)に分離されており、scripts/tests/exportPayload.test.cjsで検証している。exportService.tsは I/O だけを持つ。 - ファイルは
cacheDirectoryにsnowlog-backup-YYYYMMDD-HHmm.jsonとして書き出し、共有シートを開く。 共有後に削除はせず、次回エクスポート時に古いバックアップを掃除する。 - 共有シートが利用できない場合は
ExportErrorを投げる。この例外のメッセージだけがユーザーに提示される。
- 設定画面の「バックアップから復元」行から実行する。ファイル選択 → 件数のプレビュー → 確認 → 書き込み。
- 検証と正規化は純粋関数
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_orderとweekStartDayのみ復元する(RESTORABLE_PREFERENCE_KEYS)。
- バックアップにはメタデータしか入っていない。 動画ファイルもサムネイル画像も含まれない。
そのため別の端末で復元してもログは戻るが動画は再生できない。同一端末での再インストールなら完全に戻る。
v2 で
managedVideoPathが入るようになったが、これはパスであってバイトではない。復元先に実体が 無ければ、その行は「アプリ内の動画が見つからない」状態で戻る。写真ライブラリ側に原本が残っていてもcopy行は参照方式へ降格しない——指しているのはアプリのコピーであって原本ではないため。 - 動画の再リンク(新しい端末の写真ライブラリと突き合わせて
assetIdを張り直す)は未実装。 これが入るまで、本機能を「機種変更対応」と説明してはいけない。 - エクスポート・インポートとも実機での実行実績がまだ無い。
expo-document-pickerはネイティブモジュールのため dev client の再ビルドが要る。詳細は.memory/doc-drift.md。 applyImportPlan()はアトミックではない。6 テーブルに順に書くため途中失敗で一部だけ入る。 全書き込みが insert-or-ignore なので、やり直しが安全であることが緩和策になっている。
- 主なターゲットプラットフォーム。
- iCloud 上のアセット取得に対応。
- 詳細画面では、条件付きで写真アプリへ戻るリンクを表示する。
android.packageを設定済み。content://URI を含むインポート経路など、一部の実装下地は存在する。- ただし、Google Play 配布・実機検証・UI 調整は未実施であり、現時点のサポート対象ではない。
- iOS ユーザーが増え、プロダクトとして十分に成長した段階で Android 版を検討する。
- SQLite / MediaLibrary / Sharing の制約上、本番相当の機能提供はしない。
*.web.ts/*.web.tsxではモックやアラートベースの代替実装を用いる。- 目的は、主にレイアウト確認と簡易 UI プレビューである。
古い仕様書との差分として、特に以下は現行実装で重要である。
- タブは 4 つではなく 5 タブ(
dashboardを含む) - 日記機能
diary_entriesが存在する - 動画は
isFavoriteを持つ - 一括選択・一括お気に入り・一括削除がホームにある
- 重複候補確認画面が設定にある
- Android は未対応だが、将来対応を見据えた一部の下地がある
- JSON バックアップ基盤が実装済みである
- 日英 2 言語の i18n 基盤が導入済みで、端末の言語設定に追従する(アプリ内に切替 UI は無い)
- ハプティクスが主要操作(一括選択開始、お気に入りトグル、削除確定、インポート完了など)に配線済み
- サムネイルは
documentDirectory相対パスで保存され、iOS のコンテナ再配置に耐性がある - ホームに並び順切替とスワイプ削除が組み込まれている
- 一括インポートの結果はホームへの復帰時にサマリーアラートとして提示される
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 で型を強制し、キー欠落をコンパイル時に検出する |
src/i18n/index.tsがモジュール初期化時にexpo-localizationのgetLocales()[0]?.languageCodeを 一度だけ 読む。ja以外はすべてenに正規化し、その値をi18n-jsに固定する。- 永続化は行わない。
app_preferencesに言語のキーは存在しない。
- 言語ピッカーはコミット
bab0b45で削除済み。切替 API も setter も存在しない。 - ユーザーは iOS の設定(設定 → SnowLog → 言語)で切り替える。アプリはそこで再起動されるため、実行中にロケールが変わることはない。
- したがって
useTranslationは再レンダー用の購読機構を持たず、{ t, locale }だけを返す。
- 写真ライブラリの
NSPhotoLibraryUsageDescriptionなどのパーミッション文言は、リポジトリ直下のlocales/ja.jsonおよびlocales/en.jsonで言語別に定義する。 app.jsonのexpo.localesブロックがこれを iOS のリソースバンドルに配線する。app.jsonのinfoPlist直下に書かれた値は 日本語のフォールバック値 として残しているのみで、ローカライズの一次ソースではない。CFBundleLocalizations: ["ja", "en"]をinfoPlistに追加してあり、App Store 上で英語ストア向けにフォールバック表示される。
- ゲレンデ名(
src/constants/skiResorts.json、378 件)と都道府県名は日本語固定。 - ユーザーが設定画面で編集した滑走種別・タグ名・お気に入りゲレンデ名は、UI 言語に関係なく入力時の言語のまま表示する。
expo-haptics の native module は、新依存を取り込んだ後の dev client に古いバイナリが残っていると UnavailabilityError を投げる。これがアプリクラッシュへつながらないよう、hapticsService.ts では safeFire ラッパを介してすべての呼び出しを行い、同期 throw / Promise reject の双方を吸収する。
| 関数 | 用途 |
|---|---|
hapticLight() |
お気に入りトグル等の軽い肯定アクション |
hapticMedium() |
長押しで一括選択モードに入る等のモード遷移 |
hapticSelection() |
ピッカー / セグメント切替の確定 |
hapticSuccess() |
バルクインポート完了等、複数ステップの成功 |
hapticWarning() |
削除確定など、影響度の大きい操作の確定 |
hapticError() |
操作の失敗通知 |
主要操作にのみ配線する方針で、リスト項目の単純タップやスクロール等のジェスチャには付与しない。具体的には、お気に入り切替・一括選択開始・削除確定・インポート完了・ピッカー切替等を対象としている。
以下の変更が入った場合は、本書も更新対象とする。
- タブ構成の変更
- DB スキーマ変更
- インポート / 削除 / バックアップ仕様の変更
- 新しい設定画面の追加
- プラットフォーム対応範囲の変更
- ダッシュボード集計項目の変更
本書は README より技術寄り、コードコメントより俯瞰的、実装よりも先回りしすぎないレベルで維持するのが望ましい。