Files
jrshikoku/components/AndroidWidget/FelicaQuickAccessWidget.tsx

159 lines
6.3 KiB
TypeScript

import React from "react";
import dayjs from "dayjs";
import { FlexWidget, OverlapWidget, SvgWidget, TextWidget } from "react-native-android-widget";
import { AS } from "../../storageControl";
import { STORAGE_KEYS } from "../../constants";
import { WidgetColors, widgetLightColors } from "./widget-theme";
type LastFelicaSnapshot = {
balance: number;
idm: string;
systemCode?: string;
scannedAt: string;
};
export async function getFelicaQuickAccessData() {
const nowText = dayjs().format("HH:mm");
const snapshot = (await AS.getItem(STORAGE_KEYS.FELICA_LAST_SNAPSHOT).catch(
() => null
)) as LastFelicaSnapshot | null;
const hasBalance =
snapshot != null && typeof snapshot.balance === "number" && snapshot.balance >= 0;
return {
nowText,
amountText: hasBalance ? `\u00A5${snapshot.balance.toLocaleString()}` : "未読取",
detailText:
snapshot?.scannedAt != null
? `最終読取: ${snapshot.scannedAt}`
: "カードをタップして読取開始",
};
}
// IC card + NFC arcs, -22° rotated, as widget background
const IC_CARD_BG_SVG_LIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 160">
<g transform="translate(168,-32) rotate(-22)" opacity="0.24">
<rect x="0" y="0" width="208" height="132" rx="14" fill="#0099CC"/>
<rect x="0" y="28" width="208" height="32" fill="#007AAA"/>
<rect x="16" y="74" width="38" height="26" rx="4" fill="#FFD966"/>
<line x1="16" y1="87" x2="54" y2="87" stroke="#C8A800" stroke-width="1.5"/>
<line x1="29" y1="74" x2="29" y2="100" stroke="#C8A800" stroke-width="1.5"/>
<line x1="40" y1="74" x2="40" y2="100" stroke="#C8A800" stroke-width="1.5"/>
<circle cx="68" cy="88" r="5" fill="white" opacity="0.55"/>
<circle cx="82" cy="88" r="5" fill="white" opacity="0.55"/>
<circle cx="96" cy="88" r="5" fill="white" opacity="0.55"/>
<circle cx="110" cy="88" r="5" fill="white" opacity="0.55"/>
<circle cx="154" cy="80" r="7" fill="white" opacity="0.65"/>
<path d="M167 63 A20 20 0 0 1 167 97" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.68"/>
<path d="M178 52 A34 34 0 0 1 178 108" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.50"/>
<path d="M189 41 A48 48 0 0 1 189 119" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.34"/>
</g>
<g transform="translate(4,80) rotate(-22)" stroke="#0A88CC" fill="none" stroke-linecap="round">
<circle cx="16" cy="42" r="5" fill="#0A88CC" stroke="none" opacity="0.23"/>
<path d="M28 30 A17 17 0 0 1 28 54" stroke-width="5" opacity="0.20"/>
<path d="M38 22 A27 27 0 0 1 38 62" stroke-width="5" opacity="0.15"/>
<path d="M48 14 A37 37 0 0 1 48 70" stroke-width="5" opacity="0.10"/>
</g>
</svg>`;
const IC_CARD_BG_SVG_DARK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 160">
<g transform="translate(168,-32) rotate(-22)" opacity="0.30">
<rect x="0" y="0" width="208" height="132" rx="14" fill="#006688"/>
<rect x="0" y="28" width="208" height="32" fill="#005566"/>
<rect x="16" y="74" width="38" height="26" rx="4" fill="#AA8833"/>
<line x1="16" y1="87" x2="54" y2="87" stroke="#887722" stroke-width="1.5"/>
<line x1="29" y1="74" x2="29" y2="100" stroke="#887722" stroke-width="1.5"/>
<line x1="40" y1="74" x2="40" y2="100" stroke="#887722" stroke-width="1.5"/>
<circle cx="68" cy="88" r="5" fill="#8ecfff" opacity="0.35"/>
<circle cx="82" cy="88" r="5" fill="#8ecfff" opacity="0.35"/>
<circle cx="96" cy="88" r="5" fill="#8ecfff" opacity="0.35"/>
<circle cx="110" cy="88" r="5" fill="#8ecfff" opacity="0.35"/>
<circle cx="154" cy="80" r="7" fill="#8ecfff" opacity="0.40"/>
<path d="M167 63 A20 20 0 0 1 167 97" fill="none" stroke="#8ecfff" stroke-width="5" stroke-linecap="round" opacity="0.45"/>
<path d="M178 52 A34 34 0 0 1 178 108" fill="none" stroke="#8ecfff" stroke-width="5" stroke-linecap="round" opacity="0.30"/>
<path d="M189 41 A48 48 0 0 1 189 119" fill="none" stroke="#8ecfff" stroke-width="5" stroke-linecap="round" opacity="0.18"/>
</g>
<g transform="translate(4,80) rotate(-22)" stroke="#337799" fill="none" stroke-linecap="round">
<circle cx="16" cy="42" r="5" fill="#337799" stroke="none" opacity="0.23"/>
<path d="M28 30 A17 17 0 0 1 28 54" stroke-width="5" opacity="0.20"/>
<path d="M38 22 A27 27 0 0 1 38 62" stroke-width="5" opacity="0.15"/>
<path d="M48 14 A37 37 0 0 1 48 70" stroke-width="5" opacity="0.10"/>
</g>
</svg>`;
export function FelicaQuickAccessWidget({
amountText,
detailText,
colors = widgetLightColors,
}: {
amountText: string;
nowText: string;
detailText: string;
colors?: WidgetColors;
}) {
const hasValue = amountText !== "未読取";
const isDark = colors.background !== "#ffffff" && colors.background !== "#DDF2FF";
return (
<OverlapWidget
style={{
height: "match_parent",
width: "match_parent",
backgroundColor: colors.felicaBg,
borderRadius: 20,
overflow: "hidden",
}}
clickAction="OPEN_URI"
clickActionData={{ uri: "jrshikoku://open/felica" }}
>
{/* Background: IC card + NFC icons, rotated */}
<SvgWidget
style={{ height: "match_parent", width: "match_parent" }}
svg={isDark ? IC_CARD_BG_SVG_DARK : IC_CARD_BG_SVG_LIGHT}
/>
{/* Foreground: balance only */}
<FlexWidget
style={{
height: "match_parent",
width: "match_parent",
justifyContent: "center",
alignItems: "center",
paddingLeft: 16,
paddingRight: 16,
}}
>
<TextWidget
text={amountText}
style={{
fontSize: 48,
fontWeight: "bold",
color: hasValue ? colors.felicaText : colors.felicaTextMuted,
}}
/>
</FlexWidget>
{/* Bottom-right: scan timestamp */}
{hasValue && (
<FlexWidget
style={{
height: "match_parent",
width: "match_parent",
justifyContent: "flex-end",
alignItems: "flex-end",
paddingRight: 10,
paddingBottom: 8,
}}
>
<TextWidget
text={detailText}
style={{
fontSize: 11,
color: colors.felicaDetail,
}}
/>
</FlexWidget>
)}
</OverlapWidget>
);
}