posimai-veil/index.html

990 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-veil">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('posimai-veil-theme') || 'system';
var dark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', t);
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="ホーム画面をシンプルに保つアプリランチャー">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0D0D0D" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#F9FAFB" media="(prefers-color-scheme: light)">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Veil">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="/logo.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>Posimai Veil</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<style>
/* ── Clock zone ── */
.veil-clock {
padding: 28px 20px 16px;
position: relative;
}
.veil-time {
font-size: 68px;
font-weight: 300;
letter-spacing: -3px;
line-height: 1;
color: var(--text);
}
.veil-sub {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
font-size: 13px;
color: var(--text2);
margin-top: 8px;
}
.veil-sep { color: var(--text3); }
.veil-weather {
display: inline-flex;
align-items: center;
gap: 4px;
}
.veil-weather-icon {
width: 14px;
height: 14px;
stroke: var(--text2);
stroke-width: 1.75;
}
.veil-greeting {
font-size: 13px;
color: var(--text3);
margin-top: 4px;
}
.veil-settings-btn {
position: absolute;
top: 20px;
right: 12px;
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.12s;
}
.veil-settings-btn:active { background: var(--surface2); }
/* ── Search ── */
.search-wrap {
padding: 8px 16px 4px;
position: sticky;
top: 0;
z-index: 10;
background: var(--bg);
}
.search-wrap-inner { position: relative; }
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text3);
pointer-events: none;
display: flex;
}
.search-input {
width: 100%;
box-sizing: border-box;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 14px;
padding: 9px 14px 9px 38px;
outline: none;
font-family: inherit;
transition: border-color 0.15s;
}
.search-input:focus { border-color: var(--accent); }
/* ── Category tabs ── */
.cat-scroll {
display: flex;
gap: 6px;
padding: 8px 16px;
overflow-x: auto;
scrollbar-width: none;
position: sticky;
top: 52px;
z-index: 9;
background: var(--bg);
}
.cat-scroll::-webkit-scrollbar { display: none; }
.cat-btn {
flex-shrink: 0;
padding: 5px 12px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text2);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: background 0.12s, color 0.12s, border-color 0.12s;
white-space: nowrap;
}
.cat-btn.active {
background: var(--accent);
color: #0D0D0D;
border-color: var(--accent);
font-weight: 600;
}
/* ── Edit bar ── */
.edit-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 16px 0;
min-height: 32px;
}
.edit-bar-hint { font-size: 11px; color: var(--text3); }
.edit-toggle-btn {
font-size: 12px;
padding: 4px 12px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text2);
font-family: inherit;
cursor: pointer;
transition: all 0.12s;
}
.edit-toggle-btn.active {
border-color: var(--accent);
color: var(--accent);
background: var(--surface);
}
/* ── App sections ── */
.app-section { padding: 0 16px 16px; }
.section-label {
font-size: 11px;
font-weight: 600;
color: var(--text3);
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 12px 0 8px;
}
.app-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
/* ── App item ── */
.app-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 6px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
color: var(--text);
transition: background 0.12s, border-color 0.12s, transform 0.1s;
-webkit-tap-highlight-color: transparent;
user-select: none;
position: relative;
}
.app-item:active { background: var(--surface2); transform: scale(0.94); }
.app-item.hidden { display: none; }
.app-item.edit-mode { cursor: pointer; }
.app-item.edit-mode.off { opacity: 0.35; }
.app-item.edit-mode.on { border-color: var(--accent); }
.app-icon {
width: 26px;
height: 26px;
stroke: var(--accent);
stroke-width: 1.5;
flex-shrink: 0;
}
.app-label {
font-size: 10px;
font-weight: 500;
color: var(--text2);
text-align: center;
line-height: 1.3;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.check-badge {
display: none;
position: absolute;
top: 5px;
right: 5px;
width: 14px;
height: 14px;
background: var(--accent);
border-radius: 50%;
align-items: center;
justify-content: center;
}
.app-item.edit-mode.on .check-badge { display: flex; }
/* ── Empty state ── */
.empty-state {
text-align: center;
padding: 60px 24px;
color: var(--text3);
}
.empty-icon {
width: 36px;
height: 36px;
stroke-width: 1.25;
margin: 0 auto 12px;
display: block;
opacity: .45;
}
.empty-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.empty-sub { font-size: 13px; }
/* ── Color mode selector ── */
.color-mode-selector {
display: flex;
gap: 6px;
margin-top: 8px;
}
.color-mode-btn {
flex: 1;
padding: 7px 8px;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: var(--text2);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.12s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.color-mode-btn.active {
background: var(--surface2);
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
.color-dots {
display: flex;
gap: 2px;
}
.color-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
/* ── Settings extras ── */
.settings-field-label {
font-size: 12px;
color: var(--text2);
margin-top: 4px;
line-height: 1.5;
}
.settings-text-input {
width: 100%;
box-sizing: border-box;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 13px;
padding: 9px 12px;
outline: none;
font-family: inherit;
margin-top: 6px;
}
.settings-text-input:focus { border-color: var(--accent); }
.settings-action-btn {
width: 100%;
padding: 10px;
margin-top: 8px;
background: var(--accent);
color: #0D0D0D;
border: none;
border-radius: 8px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.settings-reset-btn {
width: 100%;
padding: 9px;
margin-top: 6px;
background: transparent;
color: var(--text3);
border: 1px solid var(--border);
border-radius: 8px;
font-family: inherit;
font-size: 12px;
cursor: pointer;
}
/* ── Safe area ── */
main { padding-bottom: max(16px, env(safe-area-inset-bottom)); }
</style>
</head>
<body>
<a href="#main-content" class="skip-link" tabindex="0"
style="position:absolute;top:-100%;left:8px;background:var(--accent);color:#0D0D0D;padding:8px 16px;border-radius:8px;font-weight:600;font-size:13px;z-index:10000;text-decoration:none">
コンテンツへスキップ
</a>
<!-- Settings panel -->
<aside class="settings-panel" id="settingsPanel" role="complementary">
<div class="settings-panel-header">
<span class="settings-panel-title">設定</span>
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
</button>
</div>
<div class="settings-panel-body">
<div>
<div class="settings-group-label">外観</div>
<div class="settings-item">
<div class="settings-item-label">テーマ</div>
<div class="theme-selector">
<button class="theme-btn" data-theme-val="dark">
<i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.75"></i>ダーク
</button>
<button class="theme-btn" data-theme-val="light">
<i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.75"></i>ライト
</button>
<button class="theme-btn" data-theme-val="system">
<i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.75"></i>自動
</button>
</div>
</div>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">アイコンカラー</div>
<div class="color-mode-selector">
<button class="color-mode-btn" data-color-mode="accent" id="colorModeAccent">
<span class="color-dots">
<span class="color-dot" style="background:#6EE7B7"></span>
<span class="color-dot" style="background:#6EE7B7"></span>
<span class="color-dot" style="background:#6EE7B7"></span>
</span>
統一色
</button>
<button class="color-mode-btn" data-color-mode="colorful" id="colorModeColorful">
<span class="color-dots">
<span class="color-dot" style="background:#60A5FA"></span>
<span class="color-dot" style="background:#F472B6"></span>
<span class="color-dot" style="background:#4ADE80"></span>
</span>
カラフル
</button>
</div>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">同期(オプション)</div>
<p class="settings-field-label">
Posimai API キーを設定すると、複数端末でアプリ設定を共有できます。
</p>
<input class="settings-text-input" id="apiKeyInput" type="password"
placeholder="posimai_api_key" autocomplete="off">
<button class="settings-action-btn" id="apiKeySave">保存</button>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">リセット</div>
<button class="settings-reset-btn" id="resetBtn">表示アプリをすべてデフォルトに戻す</button>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<main id="main-content">
<!-- 時計・天気・挨拶ゾーン -->
<div class="veil-clock">
<div class="veil-time" id="veilTime">--:--</div>
<div class="veil-sub">
<span id="veilDate">---</span>
<span class="veil-sep">·</span>
<span class="veil-weather" id="veilWeather">
<i data-lucide="cloud" class="veil-weather-icon" id="veilWeatherIcon"></i>
<span id="veilTemp">--°</span>
<span id="veilWeatherLabel"></span>
</span>
</div>
<div class="veil-greeting" id="veilGreeting"></div>
<button class="veil-settings-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:16px;height:16px;stroke-width:1.5"></i>
</button>
</div>
<div class="search-wrap">
<div class="search-wrap-inner">
<span class="search-icon">
<i data-lucide="search" style="width:16px;height:16px;stroke-width:1.75"></i>
</span>
<input class="search-input" id="searchInput" type="search"
placeholder="アプリを検索" autocomplete="off" autocorrect="off" autocapitalize="off">
</div>
</div>
<div class="cat-scroll" id="catScroll" role="tablist" aria-label="カテゴリフィルタ"></div>
<div class="edit-bar">
<span class="edit-bar-hint" id="editBarHint"></span>
<button class="edit-toggle-btn" id="editToggleBtn">編集</button>
</div>
<div id="appSections"></div>
</main>
<div id="toast" role="status" aria-live="polite"></div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
<script defer>
// ============================================================
// Posimai Veil — アプリランチャー
// ============================================================
const ENABLED_KEY = 'posimai-veil-enabled';
const API_KEY_KEY = 'posimai_api_key';
const COLOR_MODE_KEY = 'posimai-veil-color-mode';
const API_BASE = 'https://posimai-lab.tail72e846.ts.net/brain/api';
// ── カテゴリカラー(ダーク / ライト) ──────────────────────
const CAT_COLORS = {
dark: {
posimai: '#6EE7B7', // Tealブランドカラー
sns: '#60A5FA', // Blue
media: '#F472B6', // Pink
news: '#FB923C', // Orange
tools: '#94A3B8', // Slate
nav: '#4ADE80', // Green
shop: '#FBBF24', // Amber
},
light: {
posimai: '#059669', // Emerald-600
sns: '#2563EB', // Blue-600
media: '#DB2777', // Pink-600
news: '#EA580C', // Orange-600
tools: '#475569', // Slate-600
nav: '#16A34A', // Green-600
shop: '#D97706', // Amber-600
},
};
// ── アプリDB ────────────────────────────────────────────────
// android : Androidパッケージ名_camera/_phone/_settings は特殊intent
// ios : iOS URLスキーム
// web : HTTPSフォールバック
const APP_DB = [
// SNS・メッセージ
{ id:'line', label:'LINE', icon:'message-circle', cat:'sns',
android:'jp.naver.line.android', ios:'line://', web:'https://line.me' },
{ id:'twitter', label:'X', icon:'at-sign', cat:'sns',
android:'com.twitter.android', ios:'twitter://', web:'https://x.com' },
{ id:'instagram', label:'Instagram', icon:'aperture', cat:'sns',
android:'com.instagram.android', ios:'instagram://', web:'https://instagram.com' },
{ id:'tiktok', label:'TikTok', icon:'play-circle', cat:'sns',
android:'com.zhiliaoapp.musically', ios:'tiktok://', web:'https://tiktok.com' },
{ id:'facebook', label:'Facebook', icon:'users', cat:'sns',
android:'com.facebook.katana', ios:'fb://', web:'https://facebook.com' },
{ id:'whatsapp', label:'WhatsApp', icon:'message-square', cat:'sns',
android:'com.whatsapp', ios:'whatsapp://', web:'https://whatsapp.com' },
{ id:'discord', label:'Discord', icon:'headphones', cat:'sns',
android:'com.discord', ios:'discord://', web:'https://discord.com' },
{ id:'threads', label:'Threads', icon:'link-2', cat:'sns',
android:'com.instagram.barcelona', ios:null, web:'https://threads.net' },
// 動画・音楽
{ id:'youtube', label:'YouTube', icon:'play', cat:'media',
android:'com.google.android.youtube', ios:'youtube://', web:'https://youtube.com' },
{ id:'netflix', label:'Netflix', icon:'tv', cat:'media',
android:'com.netflix.mediaclient', ios:'nflx://', web:'https://netflix.com' },
{ id:'spotify', label:'Spotify', icon:'music', cat:'media',
android:'com.spotify.music', ios:'spotify://', web:'https://open.spotify.com' },
{ id:'abema', label:'AbemaTV', icon:'radio', cat:'media',
android:'tv.abema', ios:'abema://', web:'https://abema.tv' },
{ id:'tver', label:'TVer', icon:'film', cat:'media',
android:'jp.co.tver.app', ios:'tver://', web:'https://tver.jp' },
{ id:'disneyplus', label:'Disney+', icon:'star', cat:'media',
android:'com.disney.disneyplus', ios:'disneyplus://', web:'https://disneyplus.com' },
{ id:'primevideo', label:'Prime Video', icon:'tv-2', cat:'media',
android:'com.amazon.avod.thirdpartyclient', ios:'aiv://', web:'https://primevideo.com' },
{ id:'nhkplus', label:'NHK+', icon:'airplay', cat:'media',
android:'jp.nhk.player.nhkplus', ios:'nhkplus://', web:'https://plus.nhk.jp' },
// ニュース
{ id:'smartnews', label:'SmartNews', icon:'newspaper', cat:'news',
android:'jp.gocro.smartnews.android', ios:'smartnews://', web:'https://smartnews.com' },
{ id:'newspicks', label:'NewsPicks', icon:'trending-up', cat:'news',
android:'jp.co.uzabase.newspicks', ios:'newspicks://', web:'https://newspicks.com' },
{ id:'yahoonews', label:'Yahoo!ニュース', icon:'rss', cat:'news',
android:'jp.co.yahoo.android.yjtop', ios:'yjiosapp://', web:'https://news.yahoo.co.jp' },
// ショッピング・決済
{ id:'amazon', label:'Amazon', icon:'shopping-cart', cat:'shop',
android:'com.amazon.mShop.android.shopping', ios:'com.amazon.mobile.shopping://', web:'https://amazon.co.jp' },
{ id:'rakuten', label:'楽天市場', icon:'shopping-bag', cat:'shop',
android:'jp.co.rakuten.ichiba', ios:'rakuten-ichiba://', web:'https://rakuten.co.jp' },
{ id:'mercari', label:'メルカリ', icon:'tag', cat:'shop',
android:'com.kouzoh.mercari', ios:'mercari://', web:'https://mercari.com' },
{ id:'paypay', label:'PayPay', icon:'credit-card', cat:'shop',
android:'jp.ne.paypay.android', ios:'paypay://', web:'https://paypay.ne.jp' },
{ id:'rakupay', label:'楽天Pay', icon:'wallet', cat:'shop',
android:'jp.co.rakuten.pay', ios:'rakutenpay://', web:'https://pay.rakuten.co.jp' },
// マップ・移動
{ id:'gmaps', label:'Google Maps', icon:'map-pin', cat:'nav',
android:'com.google.android.apps.maps', ios:'comgooglemaps://', web:'https://maps.google.com' },
{ id:'yahoomap', label:'Yahoo!地図', icon:'map', cat:'nav',
android:'jp.co.yahoo.android.apps.ysm', ios:'yjiosapp://', web:'https://map.yahoo.co.jp' },
{ id:'navitime', label:'NAVITIME', icon:'navigation', cat:'nav',
android:'jp.co.navitime.android.townnavi', ios:'navitime://', web:'https://navitime.co.jp' },
// ツール
{ id:'gmail', label:'Gmail', icon:'mail', cat:'tools',
android:'com.google.android.gm', ios:'googlegmail://', web:'https://mail.google.com' },
{ id:'gcal', label:'カレンダー', icon:'calendar', cat:'tools',
android:'com.google.android.calendar', ios:'calshow://', web:'https://calendar.google.com' },
{ id:'gdrive', label:'Drive', icon:'hard-drive', cat:'tools',
android:'com.google.android.apps.docs', ios:'googledrive://', web:'https://drive.google.com' },
{ id:'gphoto', label:'フォト', icon:'image', cat:'tools',
android:'com.google.android.apps.photos', ios:'googlephotos://', web:'https://photos.google.com' },
{ id:'camera', label:'カメラ', icon:'camera', cat:'tools',
android:'_camera', ios:null, web:null },
{ id:'chrome', label:'Chrome', icon:'globe', cat:'tools',
android:'com.android.chrome', ios:'googlechromes://', web:'https://google.com' },
{ id:'keep', label:'Keep', icon:'file-text', cat:'tools',
android:'com.google.android.keep', ios:null, web:'https://keep.google.com' },
{ id:'phone', label:'電話', icon:'phone', cat:'tools',
android:'_phone', ios:'tel://', web:null },
{ id:'settings', label:'設定', icon:'settings-2', cat:'tools',
android:'_settings', ios:'app-settings://', web:null },
{ id:'clock', label:'時計', icon:'clock', cat:'tools',
android:'com.google.android.deskclock', ios:null, web:null },
{ id:'calc', label:'電卓', icon:'calculator', cat:'tools',
android:'com.google.android.calculator', ios:null, web:null },
// Posimai
{ id:'p-ambient', label:'Ambient', icon:'monitor', cat:'posimai',
android:null, ios:null, web:'https://posimai-ambient.vercel.app' },
{ id:'p-feed', label:'Feed', icon:'rss', cat:'posimai',
android:null, ios:null, web:'https://posimai-feed.vercel.app' },
{ id:'p-journal', label:'Journal', icon:'notebook-pen', cat:'posimai',
android:null, ios:null, web:'https://posimai-journal.vercel.app' },
{ id:'p-brain', label:'Brain', icon:'layers', cat:'posimai',
android:null, ios:null, web:'https://posimai-brain.vercel.app' },
{ id:'p-habit', label:'Habit', icon:'check-circle', cat:'posimai',
android:null, ios:null, web:'https://posimai-habit.vercel.app' },
{ id:'p-pulse', label:'Pulse', icon:'activity', cat:'posimai',
android:null, ios:null, web:'https://posimai-pulse.vercel.app' },
{ id:'p-think', label:'Think', icon:'cpu', cat:'posimai',
android:null, ios:null, web:'https://posimai-think.vercel.app' },
{ id:'p-daily', label:'Daily', icon:'mic', cat:'posimai',
android:null, ios:null, web:'https://posimai-daily.vercel.app' },
{ id:'p-timer', label:'Timer', icon:'clock', cat:'posimai',
android:null, ios:null, web:'https://posimai-timer.vercel.app' },
{ id:'p-dashboard',label:'Dashboard', icon:'layout-dashboard',cat:'posimai',
android:null, ios:null, web:'https://posimai-dashboard.vercel.app' },
];
const CATS = [
{ id:'all', label:'すべて' },
{ id:'posimai', label:'Posimai' },
{ id:'sns', label:'SNS' },
{ id:'media', label:'メディア' },
{ id:'news', label:'ニュース' },
{ id:'tools', label:'ツール' },
{ id:'nav', label:'マップ' },
{ id:'shop', label:'ショッピング' },
];
const CAT_LABELS = {
sns: 'SNS・メッセージ',
media: '動画・音楽',
news: 'ニュース',
shop: 'ショッピング・決済',
nav: 'マップ・移動',
tools: 'ツール',
posimai: 'Posimai',
};
const CAT_ORDER = ['posimai','sns','media','news','tools','nav','shop'];
// ── 状態 ────────────────────────────────────────────────────
let enabledIds = loadEnabled();
let activeCat = 'all';
let searchQ = '';
let editMode = false;
let colorMode = localStorage.getItem(COLOR_MODE_KEY) || 'accent'; // 'accent' | 'colorful'
let apiKey = localStorage.getItem(API_KEY_KEY) || '';
function getTheme() {
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
}
function getCatColor(cat) {
if (colorMode !== 'colorful') return 'var(--accent)';
return CAT_COLORS[getTheme()][cat] || 'var(--accent)';
}
function loadEnabled() {
try {
const raw = localStorage.getItem(ENABLED_KEY);
if (raw) return new Set(JSON.parse(raw));
} catch {}
return new Set(APP_DB.map(a => a.id)); // デフォルト: 全ON
}
function saveEnabled() {
localStorage.setItem(ENABLED_KEY, JSON.stringify([...enabledIds]));
}
// ── アプリ起動 ──────────────────────────────────────────────
function launchApp(app) {
const ua = navigator.userAgent.toLowerCase();
const isA = /android/.test(ua);
const isI = /iphone|ipad|ipod/.test(ua);
if (isA && app.android) {
if (app.android === '_camera') {
location.href = 'intent:#Intent;action=android.media.action.IMAGE_CAPTURE;end';
} else if (app.android === '_settings') {
location.href = 'intent:#Intent;action=android.settings.SETTINGS;end';
} else if (app.android === '_phone') {
location.href = 'tel:';
} else {
location.href = `intent:#Intent;package=${app.android};action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;end`;
}
return;
}
if (isI && app.ios) {
location.href = app.ios;
if (app.web) setTimeout(() => { location.href = app.web; }, 2500);
return;
}
if (app.web) {
window.open(app.web, '_blank', 'noopener');
}
}
// ── 編集モード: 表示/非表示トグル ──────────────────────────
function toggleApp(id) {
if (enabledIds.has(id)) enabledIds.delete(id);
else enabledIds.add(id);
saveEnabled();
renderApps();
}
// ── カテゴリタブ描画 ────────────────────────────────────────
function renderCatTabs() {
const wrap = document.getElementById('catScroll');
wrap.innerHTML = CATS.map(c => `
<button class="cat-btn${activeCat === c.id ? ' active' : ''}"
data-cat="${c.id}" role="tab" aria-selected="${activeCat === c.id}">
${c.label}
</button>
`).join('');
wrap.querySelectorAll('.cat-btn').forEach(btn => {
btn.addEventListener('click', () => {
activeCat = btn.dataset.cat;
renderCatTabs();
renderApps();
});
});
}
// ── アプリグリッド描画 ──────────────────────────────────────
function renderApps() {
const container = document.getElementById('appSections');
const hint = document.getElementById('editBarHint');
const editBtn = document.getElementById('editToggleBtn');
editBtn.classList.toggle('active', editMode);
hint.textContent = editMode ? 'タップで表示/非表示を切り替え' : '';
const q = searchQ.toLowerCase();
const filtered = APP_DB.filter(app => {
if (activeCat !== 'all' && app.cat !== activeCat) return false;
if (q && !app.label.toLowerCase().includes(q) && !app.id.includes(q)) return false;
return true;
});
if (!filtered.length) {
container.innerHTML = `
<div class="empty-state">
<i data-lucide="search" class="empty-icon"></i>
<div class="empty-title">見つかりません</div>
<div class="empty-sub">キーワードを変えてみてください</div>
</div>`;
lucide.createIcons();
return;
}
const groups = {};
filtered.forEach(app => {
if (!groups[app.cat]) groups[app.cat] = [];
groups[app.cat].push(app);
});
const orderedCats = CAT_ORDER.filter(c => groups[c]);
container.innerHTML = orderedCats.map(cat => {
const apps = groups[cat];
const color = getCatColor(cat);
const isColorful = colorMode === 'colorful';
// カラフルモード時: カードに極薄の色背景+ボーダー
const cardColorStyle = isColorful
? `--item-color:${color};`
: '';
return `
<div class="app-section">
<div class="section-label"
style="${isColorful ? `color:${color};opacity:.9` : ''}">${CAT_LABELS[cat] || cat}</div>
<div class="app-grid">
${apps.map(app => {
const on = enabledIds.has(app.id);
const hidden = !editMode && !on ? ' hidden' : '';
const editCls = editMode ? ` edit-mode ${on ? 'on' : 'off'}` : '';
const bgStyle = isColorful
? `background:${color}14;border-color:${color}38;`
: '';
return `
<div class="app-item${editCls}${hidden}"
data-id="${app.id}" role="button" tabindex="0"
aria-label="${app.label}"
style="${bgStyle}${cardColorStyle}">
<i data-lucide="${app.icon}" class="app-icon"
style="stroke:${color}"></i>
<span class="app-label">${app.label}</span>
<span class="check-badge" aria-hidden="true"
style="background:${color}">
<i data-lucide="check"
style="width:9px;height:9px;stroke-width:3;stroke:#0D0D0D"></i>
</span>
</div>`;
}).join('')}
</div>
</div>`;
}).join('');
container.querySelectorAll('.app-item').forEach(el => {
const id = el.dataset.id;
const app = APP_DB.find(a => a.id === id);
if (!app) return;
el.addEventListener('click', () => {
if (editMode) toggleApp(id);
else launchApp(app);
});
el.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (editMode) toggleApp(id);
else launchApp(app);
}
});
});
lucide.createIcons();
}
// ── 編集モードトグル ────────────────────────────────────────
document.getElementById('editToggleBtn').addEventListener('click', () => {
editMode = !editMode;
renderApps();
});
// ── 検索 ────────────────────────────────────────────────────
document.getElementById('searchInput').addEventListener('input', e => {
searchQ = e.target.value;
if (searchQ) activeCat = 'all';
renderCatTabs();
renderApps();
});
// ── カラーモード切替 ────────────────────────────────────────
function syncColorModeUI() {
document.getElementById('colorModeAccent').classList.toggle('active', colorMode === 'accent');
document.getElementById('colorModeColorful').classList.toggle('active', colorMode === 'colorful');
}
['colorModeAccent', 'colorModeColorful'].forEach(id => {
document.getElementById(id).addEventListener('click', () => {
colorMode = id === 'colorModeAccent' ? 'accent' : 'colorful';
localStorage.setItem(COLOR_MODE_KEY, colorMode);
syncColorModeUI();
renderApps();
});
});
syncColorModeUI();
// ── API キー保存 ────────────────────────────────────────────
document.getElementById('apiKeyInput').value = apiKey;
document.getElementById('apiKeySave').addEventListener('click', () => {
apiKey = document.getElementById('apiKeyInput').value.trim();
localStorage.setItem(API_KEY_KEY, apiKey);
showToast('保存しました');
// TODO: アーキテクチャ確定後に site_config 同期を追加
});
// ── リセット ────────────────────────────────────────────────
document.getElementById('resetBtn').addEventListener('click', () => {
enabledIds = new Set(APP_DB.map(a => a.id));
saveEnabled();
renderApps();
showToast('リセットしました');
});
// ── トースト ────────────────────────────────────────────────
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2200);
}
// ── 時計 ────────────────────────────────────────────────────
const DAYS_JP = ['日','月','火','水','木','金','土'];
const GREETING_POOL = {
morning: ['おはようございます','いい朝ですね','さあ、始めましょうか','今日もよろしく'],
afternoon: ['こんにちは','お昼どうでしたか','午後もがんばりましょう','ひと息ついて'],
evening: ['こんばんは','お疲れさまです','今日もよく頑張りました','ゆっくり振り返りましょう'],
night: ['お疲れさまでした','ゆっくり休んでくださいね','そろそろ休みましょうか'],
latenight: ['夜更かしですね','深夜のひとときを','静かな夜ですね'],
};
function getGreetingPool(hr) {
if (hr >= 5 && hr <= 11) return GREETING_POOL.morning;
if (hr >= 12 && hr <= 16) return GREETING_POOL.afternoon;
if (hr >= 17 && hr <= 20) return GREETING_POOL.evening;
if (hr >= 21 && hr <= 23) return GREETING_POOL.night;
return GREETING_POOL.latenight;
}
const _greetIdx = Math.floor(Math.random() * 4);
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
document.getElementById('veilTime').textContent = `${h}:${m}`;
const mo = now.getMonth() + 1;
const d = now.getDate();
const day = DAYS_JP[now.getDay()];
document.getElementById('veilDate').textContent = `${mo}${d}日(${day}`;
const pool = getGreetingPool(now.getHours());
document.getElementById('veilGreeting').textContent = pool[_greetIdx % pool.length];
}
// ── 天気 ────────────────────────────────────────────────────
const WMO_MAP = [
[0, 'sun', '晴れ'],
[3, 'cloud', '曇り'],
[48, 'cloud', '霧'],
[57, 'cloud-drizzle', '霧雨'],
[67, 'cloud-rain', '雨'],
[77, 'snowflake', '雪'],
[82, 'cloud-rain', '大雨'],
[86, 'snowflake', '大雪'],
[99, 'cloud-lightning','雷雨'],
];
function wmoToInfo(code) {
for (let i = WMO_MAP.length - 1; i >= 0; i--) {
if (code >= WMO_MAP[i][0]) return { icon: WMO_MAP[i][1], label: WMO_MAP[i][2] };
}
return { icon: 'cloud', label: '---' };
}
async function fetchWeather() {
try {
let lat = 34.6937, lon = 135.5023;
try {
const pos = await new Promise((res, rej) =>
navigator.geolocation.getCurrentPosition(res, rej, { timeout: 3000 })
);
lat = pos.coords.latitude;
lon = pos.coords.longitude;
} catch { /* デフォルト使用 */ }
const ac = new AbortController();
setTimeout(() => ac.abort(), 6000);
const r = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${lat.toFixed(4)}&longitude=${lon.toFixed(4)}&current=temperature_2m,weather_code&timezone=auto`,
{ signal: ac.signal }
);
if (!r.ok) throw new Error();
const data = await r.json();
const temp = Math.round(data.current.temperature_2m);
const info = wmoToInfo(data.current.weather_code);
document.getElementById('veilWeatherIcon').setAttribute('data-lucide', info.icon);
document.getElementById('veilTemp').textContent = `${temp}°`;
document.getElementById('veilWeatherLabel').textContent = info.label;
lucide.createIcons();
} catch {
document.getElementById('veilWeather').style.display = 'none';
}
}
// ── SW 登録 ─────────────────────────────────────────────────
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
// ── 初期化 ──────────────────────────────────────────────────
updateClock();
setInterval(updateClock, 1000);
fetchWeather();
setInterval(fetchWeather, 30 * 60 * 1000);
renderCatTabs();
renderApps();
</script>
</body>
</html>