feat: カラーテーマ設定とサウンド設定機能を追加し、外部起動方法をREADMEに記載

This commit is contained in:
harukin-expo-dev-env
2026-03-26 13:55:24 +00:00
parent e1293d2500
commit 5e66fab175
8 changed files with 173 additions and 48 deletions

View File

@@ -175,7 +175,63 @@ export const API_ENDPOINTS = {
};
```
## 📱 ビルド
## <EFBFBD> 外部からのアプリ起動(ディープリンク)
本アプリは複数の方法で外部から起動・画面遷移できます。
### カスタムURLスキーム
スキーム `jrshikoku://` を使って特定の画面を直接開くことができます。
| URL | 遷移先 |
|---|---|
| `jrshikoku://open/felica` | FeliCa履歴ページ |
| `jrshikoku://open/traininfo` | 遅延速報EX |
| `jrshikoku://open/operation` | 運行情報 |
| `jrshikoku://open/settings` | 設定 |
| `jrshikoku://open/topmenu` | トップメニュー |
| `jrshikoku://positions/apps` | 走行位置 |
URLの処理は `App.tsx``routeFromUrl` で実装されています。
### Androidウィジェット
ホーム画面に配置可能なウィジェットからアプリを起動できます。
| ウィジェット名 | 説明 |
|---|---|
| `JR_shikoku_train_info` | 遅延速報EX |
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
| `JR_shikoku_felica_balance` | ICカード残高表示 |
| `JR_shikoku_info` | 運行情報 |
| `JR_shikoku_train_strange` | 怪レい列車 |
ウィジェットのタップ時は `jrshikoku://` スキームでアプリ内画面へ遷移します。
実装: `components/AndroidWidget/widget-task-handler.tsx`
### iOSウィジェットWidgetKit
WidgetKit拡張としてホーム画面ウィジェットを提供しています。Live Activities にも対応しています。
設定: `targets/widget/Info.plist`
### プッシュ通知
通知タップ時に対応する画面へ遷移します。
| アクション | 遷移先 |
|---|---|
| `delay-ex` | 遅延速報EX |
| `strange-train` | 走行位置(怪レい列車) |
| `information` | 運行情報 |
実装: `stateBox/useNotifications.tsx`
### NFCFeliCa
NFC-Fを利用した交通系ICカードの読み取りに対応していますAndroid。アプリ内からスキャンを開始する形式です。
実装: `modules/expo-felica-reader/`
## <20>📱 ビルド
```bash
# APKビルドAndroid

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Platform, LayoutAnimation, useColorScheme } from "react-native";
import { Platform, LayoutAnimation } from "react-native";
import { WebView } from "react-native-webview";
import {
@@ -18,12 +18,13 @@ import { SheetManager } from "react-native-actions-sheet";
import { useNavigation } from "@react-navigation/native";
import { useTrainMenu } from "../../stateBox/useTrainMenu";
import { useStationList } from "../../stateBox/useStationList";
import { useThemeColors } from "@/lib/theme";
export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
const { webview, currentTrain } = useCurrentTrain();
const { navigate } = useNavigation<any>();
const { favoriteStation } = useFavoriteStation();
const { isLandscape } = useDeviceOrientationChange();
const isDark = useColorScheme() === "dark";
const { isDark } = useThemeColors();
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
const { originalStationList, stationList, getInjectJavascriptAddress } =
useStationList();

View File

@@ -22,7 +22,7 @@ import {
setAudioModeAsync,
} from "expo-audio";
import type { AudioSource } from "expo-audio";
import { useThemeColors } from "@/lib/theme";
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
const versionCode = "6.2.1.1"; // Update this version code as needed
const settingsPreviewSound = require("../../assets/sound/rikka-test.mp3");
@@ -35,7 +35,7 @@ export const SettingTopPage = ({
}) => {
const { width } = useWindowDimensions();
const { expoPushToken } = useNotification();
const { colors, fixed } = useThemeColors();
const { colors, fixed, colorTheme, setColorTheme } = useThemeColors();
const navigation = useNavigation<any>();
// expo-asset でローカルパスを取得し、expo-audio に渡す
@@ -154,6 +154,7 @@ export const SettingTopPage = ({
falseText={"リンクメニュー"}
trueText={"列車位置情報"}
/>
<ColorThemePicker value={colorTheme} onChange={setColorTheme} />
<SettingList
string="お気に入り登録の並び替え"
onPress={() =>
@@ -189,6 +190,12 @@ export const SettingTopPage = ({
navigation.navigate("setting", { screen: "LauncherIconSettings" })
}
/>
<SettingList
string="サウンド設定(β)"
onPress={() =>
navigation.navigate("setting", { screen: "SoundSettings" })
}
/>
<SettingList
string="プライバシーポリシー"
onPress={() =>
@@ -268,3 +275,46 @@ const SettingList = ({ string, onPress, disabled = false }: { string: string; on
</ListItem>
);
};
const THEME_OPTIONS: { value: ColorThemePref; label: string }[] = [
{ value: "light", label: "ライト" },
{ value: "system", label: "端末依存" },
{ value: "dark", label: "ダーク" },
];
const ColorThemePicker = ({ value, onChange }: { value: ColorThemePref; onChange: (v: ColorThemePref) => void }) => {
const { colors } = useThemeColors();
return (
<View style={{ padding: 10 }}>
<Text style={{ fontSize: 20, textAlign: "center", color: colors.text, marginBottom: 8 }}>
</Text>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
{THEME_OPTIONS.map((opt) => (
<TouchableOpacity
key={opt.value}
style={{
flex: 1,
margin: 4,
paddingVertical: 10,
borderRadius: 8,
borderWidth: 2,
borderColor: value === opt.value ? "#0099CC" : colors.border,
backgroundColor: value === opt.value ? "#0099CC20" : colors.surface,
alignItems: "center",
}}
onPress={() => onChange(opt.value)}
>
<Text style={{
fontSize: 14,
fontWeight: value === opt.value ? "bold" : "normal",
color: value === opt.value ? "#0099CC" : colors.text,
}}>
{opt.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
);
};

View File

@@ -105,6 +105,9 @@ export const STORAGE_KEYS = {
/** 駅固定モード遅延速報案内機能(サウンド) */
SOUND_DELAY_ANNOUNCEMENT: 'soundDelayAnnouncement',
/** カラーテーマ設定 ("light" | "system" | "dark") */
COLOR_THEME: 'colorTheme',
} as const;
/**

View File

@@ -1,3 +1,4 @@
export { AppThemeProvider, useThemeColors } from "./useThemeColors";
export type { ColorThemePref } from "./useThemeColors";
export { lightColors, darkColors, fixedColors } from "./colors";
export type { ThemeColorScheme } from "./colors";

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, FC, PropsWithChildren, useMemo } from "react";
import React, { createContext, useContext, FC, PropsWithChildren, useMemo, useState, useEffect, useCallback } from "react";
import { useColorScheme } from "react-native";
import {
lightColors,
@@ -6,30 +6,60 @@ import {
fixedColors,
type ThemeColorScheme,
} from "./colors";
import { AS } from "@/storageControl";
import { STORAGE_KEYS } from "@/constants";
export type ColorThemePref = "light" | "system" | "dark";
type ThemeContextValue = {
colors: ThemeColorScheme;
fixed: typeof fixedColors;
isDark: boolean;
colorTheme: ColorThemePref;
setColorTheme: (v: ColorThemePref) => void;
};
const ThemeContext = createContext<ThemeContextValue>({
colors: lightColors,
fixed: fixedColors,
isDark: false,
colorTheme: "light",
setColorTheme: () => {},
});
export const AppThemeProvider: FC<PropsWithChildren> = ({ children }) => {
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const systemScheme = useColorScheme();
const [colorTheme, setColorThemeState] = useState<ColorThemePref>("light");
useEffect(() => {
AS.getItem(STORAGE_KEYS.COLOR_THEME)
.then((v: string) => {
if (v === "light" || v === "system" || v === "dark") {
setColorThemeState(v);
}
})
.catch(() => {});
}, []);
const setColorTheme = useCallback((v: ColorThemePref) => {
setColorThemeState(v);
AS.setItem(STORAGE_KEYS.COLOR_THEME, v);
}, []);
const isDark =
colorTheme === "dark" ? true
: colorTheme === "light" ? false
: systemScheme === "dark";
const value = useMemo<ThemeContextValue>(
() => ({
colors: isDark ? darkColors : lightColors,
fixed: fixedColors,
isDark,
colorTheme,
setColorTheme,
}),
[isDark]
[isDark, colorTheme, setColorTheme]
);
return (

View File

@@ -101,22 +101,21 @@ export const injectJavascriptData = ({
s.textContent = '@keyframes _jrs_blink{0%,55%{opacity:1}70%,90%{opacity:0}100%{opacity:1}}' +
'@keyframes _jrs_glow{0%,55%{box-shadow:0 0 12px 3px rgba(255,0,0,0.9)}70%,90%{box-shadow:0 0 6px 2px rgba(255,0,0,0.45)}100%{box-shadow:0 0 12px 3px rgba(255,0,0,0.9)}}'
+ (_isDark ? (
'html,body{background-color:#2b2f36!important;}'
+ '#main{background-color:#2b2f36!important;}'
'html,body{background-color:#3a3f47!important;}'
+ '#main{background-color:#3a3f47!important;}'
+ '#disp{background-color:transparent!important;}'
+ '#header{background-color:#343a42!important;color:#e6edf3!important;border-bottom:1px solid #444c56!important;}'
+ '#header{background-color:#454b54!important;color:#e6edf3!important;border-bottom:1px solid #555d66!important;}'
+ '#headerStr{color:#e6edf3!important;}'
+ '#topHeader{background-color:#343a42!important;border-bottom:1px solid #444c56!important;}'
+ '#topHeader{background-color:#454b54!important;border-bottom:1px solid #555d66!important;}'
+ '#topHeader div{color:#e6edf3!important;}'
+ '#topHeader .accordion{background-color:#3b4148!important;color:#e6edf3!important;}'
+ '[id^="stationBlock"]{background-color:rgba(0,0,0,0.55)!important;}'
+ '#disp>[id*=""]{background-color:rgba(0,0,0,0.35)!important;}'
+ '#pMENU_2,#pMENU_3,#pMENU_k{background-color:#343a42!important;border-color:#444c56!important;}'
+ '#topHeader .accordion{background-color:#4d545d!important;color:#e6edf3!important;}'
+ '${uiSetting === "tokyo" ? '[id^="stationBlock"]{background-color:rgba(0,0,0,0.45)!important;}#disp>[id*=""]{background-color:rgba(0,0,0,0.25)!important;}' : ''}'
+ '#pMENU_2,#pMENU_3,#pMENU_k{background-color:#454b54!important;border-color:#555d66!important;}'
+ '#pMENU_2 div,#pMENU_3 div,#pMENU_k div{color:#e6edf3!important;}'
+ '#modal_content{background-color:#343a42!important;color:#e6edf3!important;}'
+ '#modal_content button{background-color:#3b4148!important;color:#e6edf3!important;border-color:#444c56!important;}'
+ '.accordionClass{background-color:#343a42!important;color:#e6edf3!important;}'
+ 'select{background-color:#3b4148!important;color:#e6edf3!important;border-color:#444c56!important;}'
+ '#modal_content{background-color:#454b54!important;color:#e6edf3!important;}'
+ '#modal_content button{background-color:#4d545d!important;color:#e6edf3!important;border-color:#555d66!important;}'
+ '.accordionClass{background-color:#454b54!important;color:#e6edf3!important;}'
+ 'select{background-color:#4d545d!important;color:#e6edf3!important;border-color:#555d66!important;}'
+ '#upTrainCrossBar,#dwTrainCrossBar{opacity:0.85!important;}'
) : '');
document.head.appendChild(s);
@@ -817,7 +816,7 @@ export const injectJavascriptData = ({
gradient = getColors[0];
}
const optionalTextColor = optionalText.includes("最終") ? "red" : _t.text;
const optionalTextColor = optionalText.includes("最終") ? "red" : "black";
const unyohubFormation = getUnyohubFormation(列番データ);
const hasUnyohub = unyohubFormation !== null;
const unyohubStale = hasUnyohub && isUnyohubStale(列番データ);
@@ -862,11 +861,12 @@ export const injectJavascriptData = ({
badgeHtml += "<div style='position:absolute;" + badgePosition + ":0;" + offsetStyle + "background-color:#44bb44;border-radius:50%;border:1px solid #228822;width:19px;height:19px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;'><span style='color:white;font-size:10px;font-weight:bold;line-height:1;'>E</span></div>";
}
行き先情報.insertAdjacentHTML('beforebegin', "<div style='width:100%;display:flex;flex:1;flex-direction:"+(isLeft ? "column-reverse" : "column") + ";font-weight:bold;'>" + badgeHtml + "<p style='font-size:6px;padding:0;color:" + _t.text + ";text-align:center;'>" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "</p><div style='flex:1;'></div><p style='font-size:8px;font-weight:bold;padding:0;color:" + _t.text + ";text-align:center;'>" + (isWanman ? "ワンマン " : "") + "</p><p style='font-size:6px;font-weight:bold;padding:0;color:" + _t.text + ";text-align:center;border-style:solid;border-width: "+(!!yosan2Color ? "2px" : "0px")+";border-color:" + yosan2Color + "'>" + viaData + "</p><p style='font-size:6px;font-weight:bold;padding:0;color: " + optionalTextColor + ";text-align:center;'>" + optionalText + "</p><p style='font-size:8px;font-weight:bold;padding:0;color:" + _t.text + ";text-align:center;'>" + trainName + "</p><div style='width:100%;background:" + gradient + ";'><p style='font-size:10px;font-weight:bold;padding:0;margin:0;color:white;align-items:center;align-content:center;text-align:center;text-shadow:1px 1px 0px #00000030, -1px -1px 0px #00000030,-1px 1px 0px #00000030, 1px -1px 0px #00000030,1px 0px 0px #00000030, -1px 0px 0px #00000030,0px 1px 0px #00000030, 0px -1px 0px #00000030;'>" + (ToData ? ToData + "行" : ToData) + "</p></div><div style='width:100%;background:" + trainTypeColor + ";border-radius:"+(isLeft ? "4px 4px 0 0" : "0 0 4px 4px")+";'><p style='font-size:10px;font-weight:bold;font-style:italic;padding:0;color: white;text-align:center;'>" + trainType + "</p></div><p style='font-size:8px;font-weight:bold;padding:0;text-align:center;color: "+(hasProblem ? "red": _t.text)+"; "+(hasProblem ? "animation:_jrs_blink 3s linear infinite;" : "")+"'>" + (hasProblem ? "‼️停止中‼️" : "") + "</p></div>");
行き先情報.insertAdjacentHTML('beforebegin', "<div style='width:100%;display:flex;flex:1;flex-direction:"+(isLeft ? "column-reverse" : "column") + ";font-weight:bold;'>" + badgeHtml + "<p style='font-size:6px;padding:0;color:black;text-align:center;'>" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "</p><div style='flex:1;'></div><p style='font-size:8px;font-weight:bold;padding:0;color:black;text-align:center;'>" + (isWanman ? "ワンマン " : "") + "</p><p style='font-size:6px;font-weight:bold;padding:0;color:black;text-align:center;border-style:solid;border-width: "+(!!yosan2Color ? "2px" : "0px")+";border-color:" + yosan2Color + "'>" + viaData + "</p><p style='font-size:6px;font-weight:bold;padding:0;color: " + optionalTextColor + ";text-align:center;'>" + optionalText + "</p><p style='font-size:8px;font-weight:bold;padding:0;color:black;text-align:center;'>" + trainName + "</p><div style='width:100%;background:" + gradient + ";'><p style='font-size:10px;font-weight:bold;padding:0;margin:0;color:white;align-items:center;align-content:center;text-align:center;text-shadow:1px 1px 0px #00000030, -1px -1px 0px #00000030,-1px 1px 0px #00000030, 1px -1px 0px #00000030,1px 0px 0px #00000030, -1px 0px 0px #00000030,0px 1px 0px #00000030, 0px -1px 0px #00000030;'>" + (ToData ? ToData + "行" : ToData) + "</p></div><div style='width:100%;background:" + trainTypeColor + ";border-radius:"+(isLeft ? "4px 4px 0 0" : "0 0 4px 4px")+";'><p style='font-size:10px;font-weight:bold;font-style:italic;padding:0;color: white;text-align:center;'>" + trainType + "</p></div><p style='font-size:8px;font-weight:bold;padding:0;text-align:center;color: "+(hasProblem ? "red": "black")+"; "+(hasProblem ? "animation:_jrs_blink 3s linear infinite;" : "")+"'>" + (hasProblem ? "‼️停止中‼️" : "") + "</p></div>");
`: `
行き先情報.insertAdjacentHTML('beforebegin', "<p style='font-size:10px;font-weight:bold;padding:0;color:" + _t.text + ";'>" + returnText1 + "</p>");
行き先情報.insertAdjacentHTML('beforebegin', "<div style='display:inline-flex;flex-direction:row;'><p style='font-size:10px;font-weight: bold;padding:0;color:" + _t.text + ";'>" + (ToData ? ToData + "行 " : ToData) + "</p><p style='font-size:10px;padding:0;color:" + _t.text + ";'>" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "</p></div>");
行き先情報.insertAdjacentHTML('beforebegin', "<p style='font-size:10px;font-weight:bold;padding:0;color: "+(hasProblem ? "red": _t.text)+"; "+(hasProblem ? "animation:_jrs_blink 3s linear infinite;" : "")+"'>" + (hasProblem ? "‼️停止中‼️" : "") + "</p>");
var _defTextColor = _isDark ? 'white' : 'black';
行き先情報.insertAdjacentHTML('beforebegin', "<p style='font-size:10px;font-weight:bold;padding:0;color:" + _defTextColor + ";'>" + returnText1 + "</p>");
行き先情報.insertAdjacentHTML('beforebegin', "<div style='display:inline-flex;flex-direction:row;'><p style='font-size:10px;font-weight: bold;padding:0;color:" + _defTextColor + ";'>" + (ToData ? ToData + "行 " : ToData) + "</p><p style='font-size:10px;padding:0;color:" + _defTextColor + ";'>" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "</p></div>");
行き先情報.insertAdjacentHTML('beforebegin', "<p style='font-size:10px;font-weight:bold;padding:0;color: "+(hasProblem ? "red": _defTextColor)+"; "+(hasProblem ? "animation:_jrs_blink 3s linear infinite;" : "")+"'>" + (hasProblem ? "‼️停止中‼️" : "") + "</p>");
`}
}
`;
@@ -881,20 +881,11 @@ const setNewTrainItem = (element,hasProblem,type)=>{
const data = trainDataList.find(e => e.train_id === 列番データ);
const _bc = TRAIN_TYPE_CFG[data.type];
element.style.borderColor = _bc ? _bc.borderColor : 'black';
if (_bc) {
if (_isDark) {
if (_bc.bgColor === '#ffffffcc') { element.style.backgroundColor = _t.cardBg; }
else if (_bc.bgColor === '#ffd0a9ff') { element.style.backgroundColor = '#5c3a1acc'; }
else if (_bc.bgColor === '#c7c7c7cc') { element.style.backgroundColor = '#555b62cc'; }
else { element.style.backgroundColor = _bc.bgColor; }
} else {
element.style.backgroundColor = _bc.bgColor;
}
} else {
element.style.backgroundColor = _t.cardBg;
}
var _bgc = _bc ? _bc.bgColor : '#ffffffff';
if (_isDark && _bgc.endsWith('cc')) { _bgc = _bgc.slice(0, -2) + 'ff'; }
element.style.backgroundColor = _bgc;
}else{
element.style.backgroundColor = _t.cardBg;
element.style.backgroundColor = '#ffffffff';
if(element.getAttribute('offclick').includes("express")){
element.style.borderColor = '#ff0000ff';
}else if(element.getAttribute('offclick').includes("rapid")){
@@ -1176,13 +1167,6 @@ const setStrings = () =>{
if(i.style.backgroundColor != 'rgb(247, 247, 247)'){
i.childNodes.forEach(m=> m.style.width = '20vw')
}
if (_isDark && i.nodeType === 1) {
var _cbg = getComputedStyle(i).backgroundColor;
var _cm = _cbg && _cbg.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);
if (_cm && (+_cm[1] + +_cm[2] + +_cm[3]) > 500) {
i.style.backgroundColor = '#161b22';
}
}
})
}

View File

@@ -6,13 +6,13 @@ import React, {
FC,
} from "react";
import { useColorScheme } from "react-native";
import { ASCore } from "../storageControl";
import { getStationList2 } from "../lib/getStationList";
import { injectJavascriptData } from "../lib/webViewInjectjavascript";
import { useNotification } from "../stateBox/useNotifications";
import { useThemeColors } from "@/lib/theme";
import { STORAGE_KEYS } from "@/constants/storage";
const initialState = {
@@ -119,7 +119,7 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
uiSetting,
useUnyohub: useUnyohubSetting,
useElesite: useEleSiteSetting,
isDark: useColorScheme() === "dark",
isDark: useThemeColors().isDark,
});
useEffect(() => {