Image
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
$render(
<vstack>
<image id="image" />
</vstack>
);
Jumpstart your widget ideas with production-ready templates bundled in ScriptWidget.
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
$render(
<vstack>
<image id="image" />
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Clock Template
//
// Description: Display Clock Time
//
$render(
<vstack background="yellow" frame="max,center">
<date font="title" date="start of today" style="timer" alignment="center" />
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Shape Template
//
// Description: Shape Example
//
$render(
<vstack>
<gif file="cat" />
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Empty Template
//
// Description: Empty script for starter
//
var text = "Hello ScriptWidget :)";
$render(
<vstack>
<text font="caption">{text}</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Shape Template
//
// Description: Shape Example
//
$render(
<vstack frame="max" animation="clockCustom,20">
<rect frame="20,60" color="blue" corner="5"></rect>
<rect frame="20,60" color="red" corner="5"></rect>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
var d = new Date();
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
let day = weekday[d.getDay()];
$render(
<vstack
background="red"
frame="max,center"
>
<text font="largeTitle" color="black" padding="10">
{day}
</text>
</vstack>
);
Battery state plus brightness (iOS only).
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Battery & Brightness
//
const battery = $device.battery();
const brightness = $system.brightness();
const percent = (battery.level * 100).toFixed(0);
$render(
<vstack frame="max" padding="12" background="#1e293b">
<text font="caption" color="#94a3b8">Battery & Brightness</text>
<text font="title2" color="#e2e8f0">{percent}%</text>
<text font="caption" color="#94a3b8">State: {battery.state}</text>
<text font="caption2" color="#64748b">Brightness: {brightness >= 0 ? Math.round(brightness * 100) + "%" : "n/a"}</text>
</vstack>
);
Quote of the day from zenquotes.io.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Daily Quote (zenquotes.io)
//
const url = "https://zenquotes.io/api/today";
const result = await fetch(url);
const data = JSON.parse(result);
const quote = data && data.length ? data[0] : { q: "Stay inspired", a: "ScriptWidget" };
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Daily Quote</text>
<text font="caption" color="#e2e8f0">"{quote.q}"</text>
<text font="caption2" color="#64748b">- {quote.a}</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Template for component link
//
$render(
<vstack frame="max">
<link url="https://www.baidu.com" background="blue">
<text>Baidu</text>
</link>
<link url="https://www.qq.com" background="green">
<text>QQ</text>
</link>
<link url="https://www.163.com" background="yellow">
<text>163</text>
</link>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Battery Percent Template
//
// Description: Display system battery percentage
//
let total = $device.totalDiskSpace();
let free = $device.freeDiskSpace();
let used = total - free;
let percent = used/total;
console.log(`total = ${total}, used = ${used}, percent = ${percent}`);
$render(
<zstack frame="max" padding="12">
<circle color="yellow" stroke="20"></circle>
<circle color="green" stroke="20" trim={1-percent} rotation={90 * 3}></circle>
<text>{ `${Math.round(percent * 100)}%`} </text>
</zstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
$render(
<vstack spacing="0">
<zstack>
<image id="image0" padding="0" />
<text> Hello </text>
</zstack>
<zstack>
<image id="image1" padding="0" />
<text> World </text>
</zstack>
<zstack>
<image id="image2" padding="0" />
<text> :) </text>
</zstack>
</vstack>
);
CPU count, uptime, and memory from $system.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// System Insights
//
const memory = $system.memory();
const uptimeHours = ($system.systemUptime() / 3600).toFixed(1);
const cpuCount = $system.processorCount();
const activeCpu = $system.activeProcessorCount();
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">System Insights</text>
<text font="title3" color="#e2e8f0">{$system.platform().toUpperCase()}</text>
<text font="caption" color="#94a3b8">OS: {$system.osVersionString()}</text>
<text font="caption" color="#94a3b8">CPU: {cpuCount} ({activeCpu} active)</text>
<text font="caption" color="#94a3b8">Memory: {(memory.physical / 1024 / 1024 / 1024).toFixed(1)} GB</text>
<text font="caption" color="#94a3b8">Uptime: {uptimeHours}h</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Year Days Left Template
//
// Description: Show you how many days left this year
//
var today = new Date();
var lastDay = new Date(today.getFullYear(), 12, 31);
var perDay = 1000 * 60 * 60 * 24;
var leftDays = Math.ceil((lastDay.getTime() - today.getTime()) / perDay);
$render(
<vstack background="red" frame="max">
<text font="title3" color="white">
今年还剩
</text>
<text font="title" color="white">
{leftDays} 天
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
$render(
<vstack frame="max,center">
<zstack>
<vstack animation="clockSecond">
<rect frame="5,50" color="yellow"></rect>
<rect frame="5,50" color="clear"></rect>
</vstack>
<vstack animation="clockMinute">
<rect frame="5,40" color="blue"></rect>
<rect frame="5,40" color="clear"></rect>
</vstack>
<vstack animation="clockHour">
<rect frame="5,30" color="red"></rect>
<rect frame="5,30" color="clear"></rect>
</vstack>
</zstack>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Content Select Template
//
// Description: Choose content at runtime
//
var text = "Hello ScriptWidget :)";
var a = (
<text>a text</text>
)
var b = (
<text>b text</text>
)
var c = Math.random() * 10 % 10 > 5 ? a : b;
const widget_size = $getenv("widget-size");
$render(
<vstack>
<text font="title">{text}</text>
{c}
<text font="caption">Widget Size : {widget_size}</text>
</vstack>
);
Stars, forks, and issues for any repo.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// GitHub Repo Stats
// widget-param: "owner/repo"
//
const repo = ($getenv("widget-param") || "everettjf/ScriptWidget").trim();
const url = `https://api.github.com/repos/${repo}`;
const result = await fetch(url);
const data = JSON.parse(result);
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">GitHub</text>
<text font="title3" color="#e2e8f0">{repo}</text>
<hstack spacing="12">
<stat title="Stars" value={(data.stargazers_count || 0).toString()} subtitle="" color="#f59e0b" />
<stat title="Forks" value={(data.forks_count || 0).toString()} subtitle="" color="#38bdf8" />
<stat title="Issues" value={(data.open_issues_count || 0).toString()} subtitle="" color="#ef4444" />
</hstack>
</vstack>
);
Exchange rates from open.er-api.com with a configurable base currency.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Currency Pulse (open.er-api.com)
// widget-param: base currency (optional)
//
const base = ($getenv("widget-param") || "USD").trim().toUpperCase();
const url = `https://open.er-api.com/v6/latest/${base}`;
const result = await fetch(url);
const data = JSON.parse(result);
const rates = data.rates || {};
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Currency Pulse</text>
<text font="title3" color="#e2e8f0">Base: {base}</text>
<hstack spacing="12">
<stat title="CNY" value={rates.CNY ? rates.CNY.toFixed(2) : "-"} subtitle="" color="#38bdf8" />
<stat title="EUR" value={rates.EUR ? rates.EUR.toFixed(2) : "-"} subtitle="" color="#22c55e" />
<stat title="JPY" value={rates.JPY ? rates.JPY.toFixed(2) : "-"} subtitle="" color="#f59e0b" />
</hstack>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Usage for component image
//
/*
<image />
<image systemName="mosaic.fill" />
<image id="image0" />
<image id="image" mode="fit" ratio="0.6" />
<image id="image" mode="fill" frame="260,60" />
<image id="image" mode="fill" clip frame="200,100"/>
<image id="image" mode="fill" clip="rect" frame="200,100"/>
<image id="image" mode="fill" clip="ellipse" frame="200,100"/>
<image id="image" mode="fill" clip="circle" frame="200,100"/>
<image id="image" mode="fill" clip="capsule" frame="200,100"/>
<image id="image" mode="fill" corner="30" frame="200,100"/>
*/
$render(
<vstack>
<image url="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" frame="20,20"/>
<image id="image" frame="260,60"/>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Usage for component Live Activity and Dynamic Island
//
// $render is also for lock screen live activity
$render(
<vstack frame="max">
<text>hello live activity</text>
</vstack>
);
// $dynamic_island is for dynamic island
// on iPhone 14 Pro/ProMax and iOS16.1+
$dynamic_island({
expanded: { // expanded is required , at least one of the four child below is required
leading: <text>leading</text>,
trailing: <text>trailing</text>,
center: <text>center</text>,
bottom: <text>bottom</text>,
},
compactLeading: <text>compactLeading</text>, // required
compactTrailing: <text>compactTrailing</text>, // required
minimal: <text>minimal</text>, // required
});
Track real-time crypto prices and 24h change using CoinGecko.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Crypto Price Ticker (CoinGecko)
// widget-param: coin id, e.g. "bitcoin", "ethereum"
//
const coin = ($getenv("widget-param") || "bitcoin").trim().toLowerCase();
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coin}&vs_currencies=usd&include_24hr_change=true`;
const result = await fetch(url);
const data = JSON.parse(result);
const info = data[coin] || { usd: 0, usd_24h_change: 0 };
const price = info.usd || 0;
const change = info.usd_24h_change || 0;
const changeColor = change >= 0 ? "#22c55e" : "#ef4444";
const changeLabel = change >= 0 ? "+" + change.toFixed(2) : change.toFixed(2);
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Crypto Ticker</text>
<text font="title2" color="#e2e8f0">{coin.toUpperCase()}</text>
<text font="title3" color="#38bdf8">${price.toFixed(2)}</text>
<text font="caption" color={changeColor}>{changeLabel}% 24h</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
const widget_size = $getenv("widget-size");
const widget_param = $getenv("widget-param");
const beijingDate = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }).toLocaleString();
const sanJoseDate = new Date().toLocaleString("zh-CN", { timeZone: "America/Los_Angeles" }).toLocaleString();
const newYorkDate = new Date().toLocaleString("zh-CN", { timeZone: "America/New_York" }).toLocaleString();
const sydneyDate = new Date().toLocaleString("zh-CN", { timeZone: "Australia/Sydney" }).toLocaleString();
$render(
<hstack frame="max">
<vstack alignment="leading">
<text font="title3" color="blue" font="custom,Unispace,14">Beijing:</text>
<text font="title3" color="green" font="custom,Unispace,14">San Jose:</text>
<text font="title3" color="orange" font="custom,Unispace,14">New York:</text>
<text font="title3" color="secondary" font="custom,Unispace,14">Sydney:</text>
</vstack>
<vstack alignment="leading">
<text font="title3" color="red" font="custom,Unispace,14">{beijingDate}</text>
<text font="title3" color="yellow" font="custom,Unispace,14">{sanJoseDate}</text>
<text font="title3" color="purple" font="custom,Unispace,14">{newYorkDate}</text>
<text font="title3" color="gray" font="custom,Unispace,14">{sydneyDate}</text>
</vstack>
</hstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Battery Gauge
//
var percent = $device.battery().level * 100;
percent = percent.toFixed(0);
let gaugeSections = [
{color: "yellow", value: 0.1},
{color: "blue", value: 0.2},
{color: "orange", value: 0.3},
{color: "green", value: 0.4},
];
$render(
<vstack frame="max">
<gauge
angle="260"
value={percent/100}
thickness="10"
label={percent + "%"} labelFont="caption"
title="BATTERY" titleFont="caption"
sections={$json(gaugeSections)}
>
</gauge>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
var d = new Date();
var n = d.getDay();
console.log(n);
let linearGradient = {
type: "linear",
colors: ["purple", "white"],
startPoint: "leading",
endPoint: "trailing",
};
var a = moment().endOf('month');
var b = moment();
var days = a.diff(b, 'days');
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,center"
>
<text font="largeTitle" color="black" padding="10">
{ days + " Days"}
</text>
<text font="caption" color="black" padding="0">
Until end of month
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
var d = new Date();
var n = d.getDay();
console.log(n);
let linearGradient = {
type: "linear",
colors: ["blue", "white"],
startPoint: "leading",
endPoint: "trailing",
};
var a = moment().endOf('year');
var b = moment();
var days = a.diff(b, 'days');
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,center"
>
<text font="largeTitle" color="black" padding="10">
{ days + " Days"}
</text>
<text font="caption" color="black" padding="0">
Until end of year
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
// https://www.weatherapi.com/
// please register account for your api key
const apikey = "8883e2c78d854356bc813207212502";
const city = "Beijing";
const url = `https://api.weatherapi.com/v1/current.json?key=${apikey}&q=${city}&aqi=no`;
const result = await fetch(url);
console.log(result);
const data = JSON.parse(result);
$render(
<vstack frame="max" background="#3a86ff">
<text font="title3" color="white">
City: {data.location.name}
</text>
<text font="title3" color="white">
Temp: {data.current.temp_c} - {data.current.temp_f}
</text>
<text font="title3" color="white">
Condition: {data.current.condition.text}
</text>
<text font="caption2" color="white">
Updated At: {data.current.last_updated}
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Battery Percent Template
//
var percent = $device.battery().level * 100;
percent = percent.toFixed(0);
let linearGradient = {
type: "linear",
colors: ["blue", "red", "green"],
startPoint: "topLeading",
endPoint: "bottomTrailing",
};
let radialGradient = {
type: "radial",
colors: ["orange", "red", "white"],
center: "center",
startRadius: 100,
endRadius: 470,
};
let angularGradient = {
type: "angular",
colors: ["green", "blue", "black", "green", "blue", "black", "green"],
center: "center",
};
$render(
<vstack background={$gradient(linearGradient)} frame="max">
<text font="50">🔋{percent} % </text>
</vstack>
);
Simple streak counter backed by $storage.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Habit Streak Tracker (uses $storage)
//
const today = new Date();
const todayKey = today.toISOString().slice(0, 10);
const lastDate = $storage.getString("habit.lastDate");
let streakValue = parseInt($storage.getString("habit.streak") || "0", 10);
if (lastDate !== todayKey) {
if (lastDate) {
const lastTime = new Date(lastDate + "T00:00:00Z").getTime();
const diffDays = Math.floor((today.getTime() - lastTime) / (24 * 60 * 60 * 1000));
if (diffDays === 1) {
streakValue += 1;
} else {
streakValue = 1;
}
} else {
streakValue = 1;
}
$storage.setString("habit.lastDate", todayKey);
$storage.setString("habit.streak", String(streakValue));
}
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Habit Streak</text>
<text font="title2" color="#e2e8f0">{streakValue} days</text>
<text font="caption2" color="#64748b">Last update: {todayKey}</text>
</vstack>
);
A focus cycle timer with progress gauge.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Focus Countdown (25-minute cycle)
//
const cycleMinutes = 25;
const now = new Date();
const minutesInto = now.getMinutes() % cycleMinutes;
const secondsInto = now.getSeconds();
const elapsed = minutesInto * 60 + secondsInto;
const total = cycleMinutes * 60;
const remaining = total - elapsed;
const progress = elapsed / total;
const mm = Math.floor(remaining / 60);
const ss = Math.floor(remaining % 60);
const timeText = `${mm.toString().padStart(2, "0")}:${ss.toString().padStart(2, "0")}`;
$render(
<vstack frame="max" padding="12" background="#1e293b">
<text font="caption" color="#94a3b8">Focus Timer</text>
<text font="title2" color="#e2e8f0">{timeText}</text>
<gauge
angle="260"
value={progress}
thickness="8"
label={`${Math.round(progress * 100)}%`}
labelFont="caption"
title="Session"
titleFont="caption"
/>
<text font="caption2" color="#94a3b8">Cycle: {cycleMinutes} min</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Weather Template
//
// Description: Display weather today
//
// https://www.weatherapi.com/
// please register account for your api key
const apikey = "8883e2c78d854356bc813207212502";
const city = "Beijing";
const url = `https://api.weatherapi.com/v1/current.json?key=${apikey}&q=${city}&aqi=no`;
const result = await fetch(url);
console.log(result);
const data = JSON.parse(result);
$render(
<vstack frame="max" background="#3a86ff">
<text font="title3" color="white">
Weather
</text>
<text font="caption" color="white">
City: {data.location.name}
</text>
<text font="caption" color="white">
Temp: {data.current.temp_c} - {data.current.temp_f}
</text>
<text font="caption" color="white">
Condition: {data.current.condition.text}
</text>
<text font="caption2" color="white">
Updated At: {data.current.last_updated}
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
var d = new Date();
var n = d.getDay();
console.log(n);
let linearGradient = {
type: "linear",
colors: ["yellow", "white"],
startPoint: "top",
endPoint: "bottom",
};
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,leading"
alignment="leading"
>
<hstack padding="10">
<vstack alignment="leading">
<text font="body" color="black">
{d.getFullYear()}-{d.getMonth() + 1}-{d.getDate()}
</text>
<text font="body" color="black">
Is Friday today ?
</text>
</vstack>
<spacer />
</hstack>
<spacer />
<text font="largeTitle" color="black" padding="10">
{n == 5 ? "Yes😊" : "No🤔"}
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
var d = new Date();
var n = d.getDay();
console.log(n);
let linearGradient = {
type: "linear",
colors: ["yellow", "red"],
startPoint: "top",
endPoint: "bottom",
};
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,leading"
alignment="leading"
>
<hstack padding="10">
<vstack alignment="leading">
<text font="body" color="black">
{d.getFullYear()}-{d.getMonth() + 1}-{d.getDate()}
</text>
<text font="body" color="black">
Is Working Day Today ?
</text>
</vstack>
<spacer />
</hstack>
<spacer />
<hstack alignment="center">
<text font="largeTitle" color="black" padding="10">
{(n >= 1 && n <= 5) ? "Yes⛽️⛽️⛽️" : "No😄"}
</text>
</hstack>
</vstack>
);
Current coordinates, accuracy, and timestamp from Core Location.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Location Snapshot
// Requires Location permission in the main app.
//
if (!$location.isAvailable()) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#f87171">Location Unavailable</text>
<text font="caption" color="#94a3b8">Location services are disabled.</text>
</vstack>
);
} else {
const status = $location.authorizationStatus();
const authorized = status === "authorizedWhenInUse" || status === "authorizedAlways";
if (!authorized) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#fbbf24">Permission Needed</text>
<text font="caption" color="#94a3b8">Enable Location access in the app.</text>
</vstack>
);
} else {
const location = await $location.current();
const lat = location.latitude.toFixed(4);
const lon = location.longitude.toFixed(4);
const accuracy = Math.max(0, Math.round(location.accuracy));
$render(
<vstack frame="max" padding="12" background="#111827" spacing="6">
<text font="caption" color="#94a3b8">Location Snapshot</text>
<text font="title3" color="#e2e8f0">{lat}, {lon}</text>
<text font="caption" color="#94a3b8">Accuracy: {accuracy}m</text>
<text font="caption2" color="#64748b">{location.timestamp}</text>
</vstack>
);
}
}
Daily close and change for a stock symbol via stooq.com.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Stock Snapshot (stooq.com)
// widget-param: symbol, e.g. "aapl.us"
//
const symbol = ($getenv("widget-param") || "aapl.us").trim().toLowerCase();
const url = `https://stooq.com/q/l/?s=${symbol}&i=d`;
const result = await fetch(url);
const lines = result.trim().split("\n");
let closeValue = "-";
let openValue = "-";
let dateValue = "-";
if (lines.length > 1) {
const cols = lines[1].split(",");
dateValue = cols[1] || "-";
openValue = cols[2] || "-";
closeValue = cols[5] || "-";
}
let change = "-";
if (openValue !== "-" && closeValue !== "-") {
const openNum = parseFloat(openValue);
const closeNum = parseFloat(closeValue);
if (!Number.isNaN(openNum) && !Number.isNaN(closeNum)) {
const diff = closeNum - openNum;
const pct = (diff / openNum) * 100;
change = `${diff.toFixed(2)} (${pct.toFixed(2)}%)`;
}
}
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Stock Snapshot</text>
<text font="title3" color="#e2e8f0">{symbol.toUpperCase()}</text>
<text font="caption" color="#94a3b8">Close: {closeValue}</text>
<text font="caption" color="#94a3b8">Change: {change}</text>
<text font="caption2" color="#64748b">Date: {dateValue}</text>
</vstack>
);
Daily sunrise and sunset for a location.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Sunrise & Sunset
// widget-param: "lat,lon" (optional)
//
let lat = 37.7749;
let lon = -122.4194;
const param = $getenv("widget-param");
if (param) {
const parts = param.split(",").map((p) => p.trim());
if (parts.length >= 2) {
const latValue = parseFloat(parts[0]);
const lonValue = parseFloat(parts[1]);
if (!Number.isNaN(latValue) && !Number.isNaN(lonValue)) {
lat = latValue;
lon = lonValue;
}
}
}
const url = `https://api.sunrise-sunset.org/json?lat=${lat}&lng=${lon}&formatted=0`;
const result = await fetch(url);
const data = JSON.parse(result);
const sunrise = data.results?.sunrise ? new Date(data.results.sunrise) : null;
const sunset = data.results?.sunset ? new Date(data.results.sunset) : null;
const formatTime = (date) => {
if (!date) return "-";
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
};
$render(
<vstack frame="max" padding="12" background="#1e293b">
<text font="caption" color="#94a3b8">Sunrise & Sunset</text>
<hstack spacing="12">
<label title={formatTime(sunrise)} systemName="sunrise.fill" color="#f59e0b" />
<label title={formatTime(sunset)} systemName="sunset.fill" color="#f97316" />
</hstack>
</vstack>
);
Countdown to a scheduled time via widget-param.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Meeting Countdown
// widget-param: "2026-02-01 09:30" or ISO string
//
const param = ($getenv("widget-param") || "").trim();
let target = null;
if (param) {
const normalized = param.includes("T") ? param : param.replace(" ", "T");
const parsed = new Date(normalized);
if (!Number.isNaN(parsed.getTime())) {
target = parsed;
}
}
if (!target) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Meeting Countdown</text>
<text font="title3" color="#e2e8f0">Set widget-param</text>
<text font="caption2" color="#64748b">Example: 2026-02-01 09:30</text>
</vstack>
);
} else {
const now = new Date();
const diffMs = target.getTime() - now.getTime();
const diffMin = Math.max(0, Math.floor(diffMs / 60000));
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
const hours = diffHour % 24;
const minutes = diffMin % 60;
$render(
<vstack frame="max" padding="12" background="#1e293b">
<text font="caption" color="#94a3b8">Next Meeting</text>
<text font="title2" color="#e2e8f0">{diffDay}d {hours}h {minutes}m</text>
<text font="caption2" color="#64748b">Target: {target.toLocaleString()}</text>
</vstack>
);
}
AQI, PM2.5, and PM10 from Open-Meteo. Optional lat/lon widget param.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Air Quality Now - Open-Meteo (no API key)
// widget-param: "lat,lon" (optional)
//
let lat = 37.7749;
let lon = -122.4194;
const param = $getenv("widget-param");
if (param) {
const parts = param.split(",").map((p) => p.trim());
if (parts.length >= 2) {
const latValue = parseFloat(parts[0]);
const lonValue = parseFloat(parts[1]);
if (!Number.isNaN(latValue) && !Number.isNaN(lonValue)) {
lat = latValue;
lon = lonValue;
}
}
}
const url = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}¤t=pm2_5,pm10,us_aqi&timezone=auto`;
const result = await fetch(url);
const data = JSON.parse(result);
const current = data.current || {};
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Air Quality</text>
<text font="title2" color="#e2e8f0">AQI {current.us_aqi ?? "-"}</text>
<hstack spacing="12">
<vstack alignment="leading">
<text font="caption2" color="#94a3b8">PM2.5</text>
<text font="caption" color="#e2e8f0">{current.pm2_5 ?? "-"}</text>
</vstack>
<vstack alignment="leading">
<text font="caption2" color="#94a3b8">PM10</text>
<text font="caption" color="#e2e8f0">{current.pm10 ?? "-"}</text>
</vstack>
</hstack>
</vstack>
);
Open-Meteo weather using the current device location.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Local Weather (Location)
// Requires Location permission in the main app.
//
if (!$location.isAvailable()) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#f87171">Location Unavailable</text>
<text font="caption" color="#94a3b8">Location services are disabled.</text>
</vstack>
);
return;
}
const status = $location.authorizationStatus();
const authorized = status === "authorizedWhenInUse" || status === "authorizedAlways";
if (!authorized) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#fbbf24">Permission Needed</text>
<text font="caption" color="#94a3b8">Enable Location access in the app.</text>
</vstack>
);
return;
}
const location = await $location.current();
const lat = location.latitude.toFixed(4);
const lon = location.longitude.toFixed(4);
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,apparent_temperature,weather_code&timezone=auto`;
const result = await fetch(url);
const data = JSON.parse(result);
const current = data.current || {};
$render(
<vstack frame="max" padding="12" background="#0f172a" spacing="6">
<text font="caption" color="#94a3b8">Local Weather</text>
<text font="title2" color="#e2e8f0">{current.temperature_2m ?? "-"} deg C</text>
<text font="caption" color="#94a3b8">Feels like {current.apparent_temperature ?? "-"} deg C</text>
<text font="caption2" color="#64748b">Weather code: {current.weather_code ?? "-"}</text>
</vstack>
);
Compass direction and speed from Core Location.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Location Compass
// Requires Location permission in the main app.
//
const directionFromCourse = (course) => {
if (course < 0 || Number.isNaN(course)) return "-";
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"];
const index = Math.round(course / 45);
return dirs[index];
};
if (!$location.isAvailable()) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#f87171">Location Unavailable</text>
<text font="caption" color="#94a3b8">Location services are disabled.</text>
</vstack>
);
return;
}
const status = $location.authorizationStatus();
const authorized = status === "authorizedWhenInUse" || status === "authorizedAlways";
if (!authorized) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#fbbf24">Permission Needed</text>
<text font="caption" color="#94a3b8">Enable Location access in the app.</text>
</vstack>
);
return;
}
const location = await $location.current();
const course = location.course;
const speed = location.speed;
$render(
<vstack frame="max" padding="12" background="#111827" spacing="6">
<text font="caption" color="#94a3b8">Compass</text>
<text font="title2" color="#e2e8f0">{directionFromCourse(course)}</text>
<text font="caption" color="#94a3b8">Course: {course >= 0 ? Math.round(course) + " deg" : "-"}</text>
<text font="caption" color="#94a3b8">Speed: {speed >= 0 ? speed.toFixed(1) + " m/s" : "-"}</text>
</vstack>
);
Shows today's steps with a configurable goal (requires read-only HealthKit authorization).
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Health Steps Ring
// Requires read-only HealthKit permission in the main app.
//
const goal = 8000;
if (!$health.isAvailable()) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#f87171">HealthKit Unavailable</text>
<text font="caption" color="#94a3b8">Check platform support.</text>
</vstack>
);
} else {
const granted = await $health.requestAuthorization();
if (!granted) {
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="title3" color="#fbbf24">Permission Needed</text>
<text font="caption" color="#94a3b8">Enable Health access in the app.</text>
</vstack>
);
} else {
const steps = await $health.stepCountToday();
const value = steps.value || 0;
const progress = Math.min(1, value / goal);
$render(
<vstack frame="max" padding="12" background="#0f172a">
<text font="caption" color="#94a3b8">Steps Today</text>
<gauge
angle="260"
value={progress}
thickness="10"
label={value.toFixed(0)}
labelFont="title3"
title={`Goal ${goal}`}
titleFont="caption"
/>
<text font="caption2" color="#94a3b8">{(progress * 100).toFixed(0)}% of goal</text>
</vstack>
);
}
}
Battery, storage, thermal state, and low-power mode in a compact layout.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// System Status Panel
//
const battery = $device.battery();
const totalDisk = $device.totalDiskSpace();
const freeDisk = $device.freeDiskSpace();
const usedDisk = Math.max(0, totalDisk - freeDisk);
const diskRatio = totalDisk > 0 ? usedDisk / totalDisk : 0;
const batteryRatio = battery.level || 0;
const gaugeSections = [
{ color: "#22c55e", value: 0.4 },
{ color: "#fbbf24", value: 0.3 },
{ color: "#ef4444", value: 0.3 }
];
$render(
<vstack frame="max" padding="12" background="#111827">
<text font="caption" color="#9ca3af">System Status</text>
<hstack spacing="12">
<vstack>
<gauge
angle="220"
value={batteryRatio}
thickness="8"
label={(batteryRatio * 100).toFixed(0) + "%"}
labelFont="caption2"
title="BATTERY"
titleFont="caption2"
sections={$json(gaugeSections)}
/>
</vstack>
<vstack>
<gauge
angle="220"
value={diskRatio}
thickness="8"
label={(diskRatio * 100).toFixed(0) + "%"}
labelFont="caption2"
title="STORAGE"
titleFont="caption2"
sections={$json(gaugeSections)}
/>
</vstack>
</hstack>
<text font="caption2" color="#9ca3af">Low Power: {$system.lowPowerMode() ? "On" : "Off"}</text>
<text font="caption2" color="#9ca3af">Thermal: {$system.thermalState()}</text>
</vstack>
);
Fetch live weather without an API key. Supports lat/lon via widget parameters.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Weather Now - Open-Meteo (no API key required)
// widget-param: "lat,lon" (optional)
//
let lat = 37.7749;
let lon = -122.4194;
const param = $getenv("widget-param");
if (param) {
const parts = param.split(",").map((p) => p.trim());
if (parts.length >= 2) {
const latValue = parseFloat(parts[0]);
const lonValue = parseFloat(parts[1]);
if (!Number.isNaN(latValue) && !Number.isNaN(lonValue)) {
lat = latValue;
lon = lonValue;
}
}
}
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code,wind_speed_10m&timezone=auto`;
const result = await fetch(url);
const data = JSON.parse(result);
const current = data.current || {};
const units = data.current_units || {};
const weatherMap = {
0: "Clear",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Fog",
51: "Drizzle",
61: "Rain",
71: "Snow",
80: "Showers",
95: "Thunder"
};
const weatherText = weatherMap[current.weather_code] || "Unknown";
const temperature = current.temperature_2m ?? "-";
const wind = current.wind_speed_10m ?? "-";
const time = current.time ?? "";
$render(
<vstack frame="max" padding="12" background="#0ea5e9">
<text font="caption" color="#e0f2fe">Weather Now</text>
<text font="title2" color="white">{temperature}{units.temperature_2m || ""}</text>
<text font="caption" color="#e0f2fe">{weatherText}</text>
<text font="caption2" color="#bae6fd">Wind: {wind}{units.wind_speed_10m || ""}</text>
<text font="caption2" color="#bae6fd">Updated: {time}</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Animation Aquarium
//
// Description: Animation Aquarium
//
/*
🛟
🪼 🐟🐠 🫧
🫧 🫧 🐡
🪼 🐬
🪼 🦐 🫧
🍀🪸🌿🪸⚓️🗿🐙🐙🌿🪸
*/
let fishHorizontal = {
type: "swing",
duration: 20,
direction: "horizontal", // "horizontal", "vertical"
distance: 100,
};
let fishVertical = {
type: "swing",
duration: 30,
direction: "vertical", // "horizontal", "vertical"
distance: 70,
};
let bubbleVertical = {
type: "swing",
duration: 15,
direction: "vertical", // "horizontal", "vertical"
distance: 50,
};
let linearGradient = {
type: "linear",
colors: ["#013A63", "#1E81B0", "#E0FBFC"],
startPoint: "top",
endPoint: "bottom",
};
$render(
<vstack background={$gradient(linearGradient)} frame="max" alignment="top">
<hstack alignment="leading">
<text font="body"> 🛟</text>
<spacer />
</hstack>
<hstack alignment="leading">
<text font="body"> 🪼 🐟 🐠 🫧</text>
<text font="body" animation={$animation(fishVertical)}>🐠</text>
<text font="body"> 🫧</text>
<spacer />
</hstack>
<hstack alignment="leading">
<text font="body"> 🫧</text>
<text font="body"> </text>
<text font="body" animation={$animation(bubbleVertical)}>🫧</text>
<text font="body" animation={$animation(fishHorizontal)}>🐡🐡</text>
<spacer />
</hstack>
<hstack alignment="leading">
<text font="body"> 🪼 🫧 🐬 🐬</text>
</hstack>
<hstack alignment="leading">
<text font="body"> 🪼 🫧 🦐 🫧🫧</text>
<spacer />
</hstack>
<hstack alignment="leading">
<text font="body"> 🍀 🪸🌿🪸⚓️🗿🐙🐙 🌿🌿 🌿🪸🪸</text>
<spacer />
</hstack>
</vstack>
);
ScriptWidget template.
const API_URL = "https://episodate.com/api/show-details?q="
const colors = {
primary: "#080808",
secondary: "green",
text: {
primary: "white",
secondary: "green"
}
}
const series = [
"the-lord-of-the-rings",
"house-of-the-dragon"
]
const fetchSeries = async seriesList => {
let response = []
for (let series of seriesList) {
response.push(
JSON.parse(
await fetch(API_URL + series)
)
)
}
return response
}
const getTimeLeft = airDate => {
let res
const millis = airDate - Date.now()
const secondsLeft = millis / 1000
const getDays = seconds => {
return Math.round(seconds / (3600 * 24))
}
const getHours = seconds => {
return Math.round(seconds % (3600 * 24) / 3600)
}
const getMinutes = seconds => {
return Math.round(seconds % 3600 / 60)
}
if (getDays(secondsLeft) > 0) {
res = getDays(secondsLeft) + "d "
res += getHours(secondsLeft) + "h "
} else if (getHours(secondsLeft) > 0) {
res = getHours(secondsLeft) + " h "
} else {
res = getMinutes(secondsLeft) + " m "
}
return res + "left"
}
const Logo = ({logoPath}) => {
return (
<zstack>
<image
url={logoPath}
frame="40,40,trailing"
/>
<rect
color={colors.secondary}
stroke="1"
frame="40,40"
/>
</zstack>
)
}
const Entry = ({info}) => {
const getNextEpisode = countdown =>
`s${countdown.season}e${countdown.episode}`
const nextEpisodeRemaining = countdown => {
const airDate = countdown.air_date
.replace(" ", "T") + "Z"
return getTimeLeft(new Date(airDate))
}
return (
<vstack
alignment="top"
>
<hstack
alignment="top"
>
<Logo
logoPath={info.image_path}
/>
<vstack
alignment="top"
>
<text
font="14"
frame="200,15,leading"
color={colors.text.primary}
>
{info.name}
</text>
<hstack>
<text
font="caption2"
frame="50,15,leading"
color={colors.text.secondary}
>
{
info.countdown === null ? "ended"
: getNextEpisode(info.countdown)
}
</text>
<text
frame="120,15,trailing"
font="caption2"
color={colors.text.secondary}
>
{
info.countdown === null ? ""
: nextEpisodeRemaining(info.countdown)
}
</text>
</hstack>
</vstack>
<spacer/>
</hstack>
</vstack>
)
}
const seriesJson = await fetchSeries(series)
$render(
<zstack
background={colors.primary}
>
<vstack
padding="10,10,10,20"
frame="max,top"
>
{
seriesJson?.map(series =>
<Entry
info={series.tvShow}
/>
)
}
</vstack>
</zstack>
);
ScriptWidget template.
//
// When setting up the widget provide a parameter
// in the setup dialog like Please provide a parameter like
// '2022-11-26 Vacation' if you want to count down
// or up to a date or '2022-11-26T12:35:00 Flight'
// if you want to count up or down to a specific time
// on a date.
//
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60;
const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
function is_leap(yr) {
return yr % 400 === 0 || (yr % 4 === 0 && yr % 100 !== 0);
}
function days_per_month(month, year) {
if (month === 1) {
if (is_leap(year)) {
return 29;
} else {
return 28;
}
} else {
months = [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return months[month];
}
}
function months_to_days(month) {
return Math.floor((month * 3057 - 3007) / 100);
}
function years_to_days(yr) {
return (
yr * 365 + Math.floor(yr / 4) - Math.floor(yr / 100) + Math.floor(yr / 400)
);
}
function ymd_to_days(yr, mo, day) {
scalar = day + months_to_days(mo);
if (mo > 2)
// adjust if past February
scalar -= is_leap(yr) ? 1 : 2;
yr--;
scalar += years_to_days(yr);
return scalar;
}
function determine_sign(remainder, large_unit, small_unit) {
if (remainder > large_unit - Math.floor(small_unit / 2)) {
return { text: "≈", count: 1 };
} else if (remainder > Math.floor(large_unit / 2)) {
return { text: "<", count: 1 };
} else if (remainder > Math.floor(small_unit / 2)) {
return { text: ">", count: 0 };
} else if (remainder > 0) {
return { text: "≈", count: 0 };
} else {
return { text: "", count: 0 };
}
}
function Countdown(target, countdown_to) {
// Convert target and now to tm formats
const now_date = new Date();
now_tm = {
tm_min: countdown_to === "T" ? now_date.getMinutes() : 0,
tm_hour: countdown_to === "T" ? now_date.getHours() : 0,
tm_mday: now_date.getDate(),
tm_mon: now_date.getMonth(),
tm_year: now_date.getFullYear(),
};
const target_date = target;
target_tm = {
tm_min: countdown_to === "T" ? target_date.getMinutes() : 0,
tm_hour: countdown_to === "T" ? target_date.getHours() : 0,
tm_mday: target_date.getDate(),
tm_mon: target_date.getMonth(),
tm_year: target_date.getFullYear(),
};
// Choose post-text, max_tm and min_tm
if (target_date.getTime() > now_date.getTime()) {
// Count down to
post_text = "";
max_tm = target_tm;
min_tm = now_tm;
} else {
// Count down to
post_text = "ago";
max_tm = now_tm;
min_tm = target_tm;
}
// Calculate differences in years, months, days, hours and minutes
received = min_tm.tm_min > max_tm.tm_min ? 60 : 0;
min_diff = max_tm.tm_min + received - min_tm.tm_min;
borrow = received > 0 ? 1 : 0;
received = min_tm.tm_hour + borrow > max_tm.tm_hour ? 24 : 0;
hour_diff = max_tm.tm_hour + received - min_tm.tm_hour - borrow;
borrow = received > 0 ? 1 : 0;
received =
min_tm.tm_mday + borrow > max_tm.tm_mday
? days_per_month(max_tm.tm_mon, max_tm.tm_year)
: 0;
day_diff = max_tm.tm_mday + received - min_tm.tm_mday - borrow;
borrow = received > 0 ? 1 : 0;
received = min_tm.tm_mon + borrow > max_tm.tm_mon ? 12 : 0;
month_diff = max_tm.tm_mon + received - min_tm.tm_mon - borrow;
borrow = received > 0 ? 1 : 0;
year_diff = max_tm.tm_year - min_tm.tm_year - borrow;
// Calculate total difference in seconds
diff =
ymd_to_days(max_tm.tm_year + 1900, max_tm.tm_mon + 1, max_tm.tm_mday) -
ymd_to_days(min_tm.tm_year + 1900, min_tm.tm_mon + 1, min_tm.tm_mday);
if (
min_tm.tm_hour * 100 + min_tm.tm_min >
max_tm.tm_hour * 100 + max_tm.tm_min
)
diff -= 1;
diff = diff * 24 + hour_diff;
diff = diff * 60 + min_diff;
diff = diff * 60;
if (diff == 0 || (countdown_to == 'D' && diff == SECONDS_PER_DAY)) {
// Display one word
if (diff == 0) {
if (countdown_to == 'D') {
return "Today";
} else {
return "Now";
}
} else {
if (target_date.getTime() > now_date.getTime()) {
return "Tomorrow";
} else {
return "Yesterday";
}
}
}
// Display incremental detail
count = 0;
remainder = 0;
if (
year_diff > 3 ||
(year_diff == 3 &&
(month_diff > 0 || day_diff > 0 || hour_diff > 0 || min_diff > 0))
) {
count = year_diff;
remainder =
ymd_to_days(
max_tm.tm_year - year_diff + 1900,
max_tm.tm_mon + 1,
max_tm.tm_mday
) - ymd_to_days(min_tm.tm_year + 1900, min_tm.tm_mon + 1, min_tm.tm_mday);
remainder *= SECONDS_PER_DAY;
remainder += hour_diff * SECONDS_PER_HOUR + min_diff * SECONDS_PER_MINUTE;
sign = determine_sign(
remainder,
(is_leap(max_tm.tm_year + 1900) ? 366 : 365) * SECONDS_PER_DAY,
SECONDS_PER_DAY
);
pre_text = sign.text;
count += sign.count;
unit = " years ";
} else if (
year_diff * 12 + month_diff > 3 ||
(month_diff == 3 && (day_diff > 0 || hour_diff > 0 || min_diff > 0))
) {
count = year_diff * 12 + month_diff;
remainder =
day_diff * SECONDS_PER_DAY +
hour_diff * SECONDS_PER_HOUR +
min_diff * SECONDS_PER_MINUTE;
sign = determine_sign(
remainder,
days_per_month(max_tm.tm_mon, max_tm.tm_year) * SECONDS_PER_DAY,
SECONDS_PER_DAY
);
pre_text = sign.text;
count += sign.count;
unit = " months ";
} else if (diff > 3 * 7 * SECONDS_PER_DAY) {
count = Math.floor(diff / (7 * SECONDS_PER_DAY));
remainder = diff % (7 * SECONDS_PER_DAY);
sign = determine_sign(remainder, 7 * SECONDS_PER_DAY, SECONDS_PER_DAY);
pre_text = sign.text;
count += sign.count;
unit = " weeks ";
} else if (diff > 3 * SECONDS_PER_DAY || countdown_to === "D") {
count = Math.floor(diff / SECONDS_PER_DAY);
remainder = diff % SECONDS_PER_DAY;
sign = determine_sign(remainder, SECONDS_PER_DAY, SECONDS_PER_HOUR);
pre_text = sign.text;
count += sign.count;
unit = " days ";
} else if (diff > 3 * SECONDS_PER_HOUR) {
count = Math.floor(diff / SECONDS_PER_HOUR);
remainder = diff % SECONDS_PER_HOUR;
sign = determine_sign(remainder, SECONDS_PER_HOUR, SECONDS_PER_MINUTE);
pre_text = sign.text;
count += sign.count;
unit = " hours ";
} else if (diff > SECONDS_PER_MINUTE) {
count = Math.floor(diff / SECONDS_PER_MINUTE);
pre_text = "";
unit = " minutes ";
} else {
count = 1;
pre_text = "";
unit = " minute ";
}
return pre_text + count + unit + post_text;
}
// Retrieve target from widget parameter
const param = $getenv("widget-param");
const dtre = /\d\d\d\d\-\d\d\-\d\d(T\d\d\:\d\d\:\d\d)?/;
dt_param = param.match(dtre);
if (!dt_param) {
$render(
<vstack frame="max">
<text>No valid widget parameter specified!</text>
<text></text>
<text font="caption">
Please provide a parameter like '2022-11-26 Vacation' or
'2022-11-26T12:35:00 Flight'
</text>
</vstack>
);
return;
} else {
target = new Date(dt_param[0]);
event = param.replace(dtre, "").trim();
countdown_to = dt_param[1] ? "T" : "D";
}
// Update widget
text = Countdown(target, countdown_to);
let linearGradient = {
type: "linear",
colors: ["#fb5a72", "#f5243b"],
startPoint: "top",
endPoint: "bottom",
};
// Date formatting
if (countdown_to === "T") {
var dateFormat = {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
} else {
var dateFormat = {
year: "numeric",
month: "short",
day: "numeric",
};
}
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,leading"
alignment="leading"
>
<hstack padding="10">
<vstack alignment="leading">
<text font="body" color="white">
{event}
</text>
<text font="caption" color="white">
{target.toLocaleDateString(undefined, dateFormat)}
</text>
</vstack>
<spacer />
</hstack>
<spacer />
<text font="title" color="white" padding="10">
{text}
</text>
</vstack>
);
ScriptWidget template.
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//
var getLunarData = (function () {
//公历农历转换
var calendar = {
lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
0x0d520],
solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
Gan: ["\u7532", "\u4e59", "\u4e19", "\u4e01", "\u620a", "\u5df1", "\u5e9a", "\u8f9b", "\u58ec", "\u7678"],
Zhi: ["\u5b50", "\u4e11", "\u5bc5", "\u536f", "\u8fb0", "\u5df3", "\u5348", "\u672a", "\u7533", "\u9149", "\u620c", "\u4ea5"],
Animals: ["\u9f20", "\u725b", "\u864e", "\u5154", "\u9f99", "\u86c7", "\u9a6c", "\u7f8a", "\u7334", "\u9e21", "\u72d7", "\u732a"],
solarTerm: ["\u5c0f\u5bd2", "\u5927\u5bd2", "\u7acb\u6625", "\u96e8\u6c34", "\u60ca\u86f0", "\u6625\u5206", "\u6e05\u660e", "\u8c37\u96e8", "\u7acb\u590f", "\u5c0f\u6ee1", "\u8292\u79cd", "\u590f\u81f3", "\u5c0f\u6691", "\u5927\u6691", "\u7acb\u79cb", "\u5904\u6691", "\u767d\u9732", "\u79cb\u5206", "\u5bd2\u9732", "\u971c\u964d", "\u7acb\u51ac", "\u5c0f\u96ea", "\u5927\u96ea", "\u51ac\u81f3"],
sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
'97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
'97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
'7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
'977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
'665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'],
nStr1: ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341"],
nStr2: ["\u521d", "\u5341", "\u5eff", "\u5345"],
nStr3: ["\u6b63", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u51ac", "\u814a"],
lYearDays: function (y) {
var i, sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) { sum += (calendar.lunarInfo[y - 1900] & i) ? 1 : 0; }
return (sum + calendar.leapDays(y));
},
leapMonth: function (y) {
return (calendar.lunarInfo[y - 1900] & 0xf);
},
leapDays: function (y) {
if (calendar.leapMonth(y)) {
return ((calendar.lunarInfo[y - 1900] & 0x10000) ? 30 : 29);
}
return (0);
},
monthDays: function (y, m) {
if (m > 12 || m < 1) { return -1 }
return ((calendar.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29);
},
solarDays: function (y, m) {
if (m > 12 || m < 1) { return -1 }
var ms = m - 1;
if (ms == 1) {
return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28);
} else {
return (calendar.solarMonth[ms]);
}
},
toGanZhi: function (offset) {
return (calendar.Gan[offset % 10] + calendar.Zhi[offset % 12]);
},
getTerm: function (y, n) {
if (y < 1900 || y > 2100) { return -1; }
if (n < 1 || n > 24) { return -1; }
var _table = calendar.sTermInfo[y - 1900];
var _info = [
parseInt('0x' + _table.substr(0, 5)).toString(),
parseInt('0x' + _table.substr(5, 5)).toString(),
parseInt('0x' + _table.substr(10, 5)).toString(),
parseInt('0x' + _table.substr(15, 5)).toString(),
parseInt('0x' + _table.substr(20, 5)).toString(),
parseInt('0x' + _table.substr(25, 5)).toString()
];
var _calday = [
_info[0].substr(0, 1),
_info[0].substr(1, 2),
_info[0].substr(3, 1),
_info[0].substr(4, 2),
_info[1].substr(0, 1),
_info[1].substr(1, 2),
_info[1].substr(3, 1),
_info[1].substr(4, 2),
_info[2].substr(0, 1),
_info[2].substr(1, 2),
_info[2].substr(3, 1),
_info[2].substr(4, 2),
_info[3].substr(0, 1),
_info[3].substr(1, 2),
_info[3].substr(3, 1),
_info[3].substr(4, 2),
_info[4].substr(0, 1),
_info[4].substr(1, 2),
_info[4].substr(3, 1),
_info[4].substr(4, 2),
_info[5].substr(0, 1),
_info[5].substr(1, 2),
_info[5].substr(3, 1),
_info[5].substr(4, 2),
];
return parseInt(_calday[n - 1]);
},
toChinaMonth: function (m) {
if (m > 12 || m < 1) { return -1 }
var s = calendar.nStr3[m - 1];
s += "\u6708";
return s;
},
toChinaDay: function (d) {
var s;
switch (d) {
case 10:
s = '\u521d\u5341';
break;
case 20:
s = '\u4e8c\u5341';
break;
case 30:
s = '\u4e09\u5341';
break;
default:
s = calendar.nStr2[Math.floor(d / 10)];
s += calendar.nStr1[d % 10];
}
return (s);
},
getAnimal: function (y) {
return calendar.Animals[(y - 4) % 12]
},
solar2lunar: function (y, m, d) {
if (y < 1900 || y > 2100) { return -1; }
if (y == 1900 && m == 1 && d < 31) { return -1; }
if (!y) {
var objDate = new Date();
} else {
var objDate = new Date(y, parseInt(m) - 1, d)
}
var i, leap = 0, temp = 0;
var y = objDate.getFullYear(), m = objDate.getMonth() + 1, d = objDate.getDate();
var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000;
for (i = 1900; i < 2101 && offset > 0; i++) { temp = calendar.lYearDays(i); offset -= temp; }
if (offset < 0) { offset += temp; i--; }
var isTodayObj = new Date(), isToday = false;
if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
isToday = true;
}
var nWeek = objDate.getDay(), cWeek = calendar.nStr1[nWeek];
if (nWeek == 0) { nWeek = 7; }
var year = i;
var leap = calendar.leapMonth(i);
var isLeap = false;
for (i = 1; i < 13 && offset > 0; i++) {
if (leap > 0 && i == (leap + 1) && isLeap == false) {
--i;
isLeap = true; temp = calendar.leapDays(year);
} else {
temp = calendar.monthDays(year, i);
}
if (isLeap == true && i == (leap + 1)) { isLeap = false; }
offset -= temp;
}
if (offset == 0 && leap > 0 && i == leap + 1) {
if (isLeap) {
isLeap = false;
} else {
isLeap = true; --i;
}
}
if (offset < 0) { offset += temp; --i; }
var month = i;
var day = offset + 1;
var sm = m - 1;
var term3 = calendar.getTerm(year, 3);
var gzY = calendar.toGanZhi(year - 4);
gzY = calendar.toGanZhi(year - 4); //modify
var firstNode = calendar.getTerm(y, (m * 2 - 1));
var secondNode = calendar.getTerm(y, (m * 2));
var gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11);
if (d >= firstNode) {
gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12);
}
var isTerm = false;
var Term = null;
if (firstNode == d) {
isTerm = true;
Term = calendar.solarTerm[m * 2 - 2];
}
if (secondNode == d) {
isTerm = true;
Term = calendar.solarTerm[m * 2 - 1];
}
var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10;
var gzD = calendar.toGanZhi(dayCyclical + d - 1);
return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': calendar.getAnimal(year), 'IMonthCn': (isLeap ? "\u95f0" : '') + calendar.toChinaMonth(month), 'IDayCn': calendar.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': "\u661f\u671f" + cWeek, 'isTerm': isTerm, 'Term': Term };
}
};
//公历节日
var _festival1 = {
'0101': '元旦节',
'0202': '世界湿地日',
'0210': '国际气象节',
'0214': '情人节',
'0301': '国际海豹日',
'0303': '全国爱耳日',
'0305': '学雷锋纪念日',
'0308': '妇女节',
'0312': '植树节',
'0314': '国际警察日',
'0315': '消费者权益日',
'0317': '中国国医节 国际航海日',
'0321': '世界森林日 消除种族歧视国际日 世界儿歌日',
'0322': '世界水日',
'0323': '世界气象日',
'0324': '世界防治结核病日',
'0325': '全国中小学生安全教育日',
'0401': '愚人节',
'0407': '世界卫生日',
'0422': '世界地球日',
'0423': '世界图书和版权日',
'0424': '亚非新闻工作者日',
'0501': '劳动节',
'0504': '青年节',
'0515': '防治碘缺乏病日',
'0508': '世界红十字日',
'0512': '国际护士节',
'0515': '国际家庭日',
'0517': '世界电信日',
'0518': '国际博物馆日',
'0520': '全国学生营养日',
'0522': '国际生物多样性日',
'0531': '世界无烟日',
'0601': '国际儿童节 世界牛奶日',
'0605': '世界环境日',
'0606': '全国爱眼日',
'0617': '防治荒漠化和干旱日',
'0623': '国际奥林匹克日',
'0625': '全国土地日',
'0626': '国际禁毒日',
'0701': '建党节 香港回归纪念日',
'0702': '国际体育记者日',
'0711': '世界人口日 航海日',
'0801': '建军节',
'0808': '中国男子节(爸爸节)',
'0903': '抗日战争胜利纪念日',
'0908': '国际扫盲日 国际新闻工作者日',
'0910': '教师节',
'0916': '国际臭氧层保护日',
'0918': '九·一八事变纪念日',
'0920': '国际爱牙日',
'0927': '世界旅游日',
'1001': '国庆节 国际音乐日 国际老人节',
'1002': '国际非暴力日 国际和平与民主自由斗争日',
'1004': '世界动物日',
'1006': '老人节',
'1008': '全国高血压日',
'1005': '国际教师节',
'1009': '世界邮政日',
'1010': '辛亥革命纪念日 世界精神卫生日',
'1013': '世界保健日 国际减灾日',
'1014': '世界标准日',
'1015': '国际盲人节(白手杖节)',
'1016': '世界粮食日',
'1017': '世界消除贫困日',
'1022': '世界传统医药日',
'1024': '联合国日 世界发展信息日',
'1031': '世界勤俭日',
'1107': '十月社会主义革命纪念日',
'1108': '中国记者日',
'1109': '全国消防安全宣传教育日',
'1110': '世界青年节',
'1111': '国际科学与和平周(本日所属的一周)',
'1112': '孙中山诞辰纪念日',
'1114': '联合国糖尿病日',
'1117': '国际大学生节',
'1121': '世界问候日 世界电视日',
'1129': '国际声援巴勒斯坦人民国际日',
'1201': '世界艾滋病日',
'1203': '世界残疾人日',
'1204': '宪法日',
'1205': '国际志愿人员日',
'1209': '世界足球日',
'1210': '世界人权日',
'1212': '西安事变纪念日',
'1213': '南京大屠杀纪念日',
'1220': '澳门回归纪念',
'1221': '国际篮球日',
'1224': '平安夜',
'1225': '圣诞节',
'1226': '毛泽东诞辰纪念日'
};
//某月的第几个星期几,第3位为5表示最后一星期
var _festival2 = {
'0110': '黑人日',
'0150': '世界麻风日',
'0440': '世界儿童日',
'0520': '国际母亲节',
'0532': '国际牛奶日',
'0530': '全国助残日',
'0630': '父亲节',
'0711': '世界建筑日',
'0730': '被奴役国家周',
'0936': '世界清洁地球日',
'0932': '国际和平日',
'0940': '国际聋人节',
'1011': '国际住房日',
'1024': '世界视觉日',
'1144': '感恩节',
'1220': '国际儿童电视广播日'
};
//农历节日
var _festival3 = {
'0101': '春节',
'0102': '初二',
'0103': '初三',
'0115': '元宵节',
'0202': '龙抬头节',
'0323': '妈祖生辰',
'0505': '端午节',
'0707': '七夕节',
'0715': '中元节',
'0815': '中秋节',
'0909': '重阳节',
'1208': '腊八节',
'1223': '小年',
'0100': '除夕'
};
//假日安排数据
var _holiday = {
'2011': { '0402': 0, '0403': 1, '0404': 1, '0405': 1, '0430': 1, '0501': 1, '0502': 1, '0604': 1, '0605': 1, '0606': 1, '0910': 1, '0911': 1, '0912': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 0, '1009': 0, '1231': 0 },
'2012': {
'0101': 1, '0102': 1, '0103': 1, '0121': 0, '0122': 1, '0123': 1, '0124': 1, '0125': 1, '0126': 1, '0127': 1, '0128': 1, '0129': 0, '0331': 0, '0401'
: 0, '0402': 1, '0403': 1, '0404': 1, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0622': 1, '0623': 1, '0624': 1, '0929': 0, '0930': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1
},
'2013': { '0101': 1, '0102': 1, '0103': 1, '0105': 0, '0106': 0, '0209': 1, '0210': 1, '0211': 1, '0212': 1, '0213': 1, '0214': 1, '0215': 1, '0216': 0, '0217': 0, '0404': 1, '0405': 1, '0406': 1, '0407': 0, '0427': 0, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0608': 0, '0609': 0, '0610': 1, '0611': 1, '0612': 1, '0919': 1, '0920': 1, '0921': 1, '0922': 0, '0929': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1012': 0 },
'2014': { '0101': 1, '0126': 0, '0131': 1, '0201': 1, '0202': 1, '0203': 1, '0203': 1, '0204': 1, '0205': 1, '0206': 1, '0208': 0, '0405': 1, '0406': 1, '0407': 1, '0501': 1, '0502': 1, '0503': 1, '0504': 0, '0531': 1, '0601': 1, '0602': 1, '0908': 1, '0928': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1011': 0 },
'2015': { '0101': 1, '0102': 1, '0103': 1, '0104': 0, '0215': 0, '0218': 1, '0219': 1, '0220': 1, '0221': 1, '0222': 1, '0223': 1, '0224': 1, '0228': 0, '0404': 1, '0405': 1, '0406': 1, '0501': 1, '0502': 1, '0503': 1, '0620': 1, '0621': 1, '0622': 1, '0903': 1, '0904': 1, '0905': 1, '0906': 0, '0927': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1010': 0 },
'2016': { '0101': 1, '0102': 1, '0103': 1, '0206': 0, '0207': 1, '0208': 1, '0209': 1, '0210': 1, '0211': 1, '0212': 1, '0213': 1, '0214': 0, '0402': 1, '0403': 1, '0404': 1, '0430': 1, '0501': 1, '0502': 1, '0609': 1, '0610': 1, '0611': 1, '0612': 0, '0915': 1, '0916': 1, '0917': 1, '0918': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 0, '1009': 0 },
'2017': { '0101': 1, '0102': 1, '0122': 0, '0127': 1, '0128': 1, '0129': 1, '0130': 1, '0131': 1, '0201': 1, '0202': 1, '0204': 0, '0401': 0, '0402': 1, '0403': 1, '0404': 1, '0429': 1, '0430': 1, '0501': 1, '0527': 0, '0528': 1, '0529': 1, '0530': 1, '0930': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 1, '1230': 1, '1231': 1 },
'2018': { '0101': 1, '0211': 0, '0215': 1, '0216': 1, '0217': 1, '0218': 1, '0219': 1, '0220': 1, '0221': 1, '0224': 0, '0405': 1, '0406': 1, '0407': 1, '0408': 0, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0616': 1, '0617': 1, '0618': 1, '0922': 1, '0923': 1, '0924': 1, '0929': 0, '0930': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1229': 0, '1230': 1, '1231': 1 },
'2019': { '0101': 1, '0202': 0, '0203': 0, '0204': 1, '0205': 1, '0206': 1, '0207': 1, '0208': 1, '0209': 1, '0210': 1, '0405': 1, '0406': 1, '0407': 1, '0428': 0, '0501': 1, '0502': 1, '0503': 1, '0504': 1, '0505': 0, '0607': 1, '0608': 1, '0609': 1, '0913': 1, '0914': 1, '0915': 1, '0929': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1012': 0 }
};
//获取日期数据
var getDateObj = function (year, month, day) {
var date = arguments.length && year ? new Date(year, month - 1, day) : new Date();
return {
'year': date.getFullYear(),
'month': date.getMonth() + 1,
'day': date.getDate(),
'week': date.getDay()
};
};
//当天
var _today = getDateObj();
//获取当月天数
var getMonthDays = function (obj) {
var day = new Date(obj.year, obj.month, 0);
return day.getDate();
};
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
}
//获取某天日期信息
var getDateInfo = function (obj) {
var info = calendar.solar2lunar(obj.year, obj.month, obj.day);
var cMonth = info.cMonth > 9 ? '' + info.cMonth : '0' + info.cMonth;
var cDay = info.cDay > 9 ? '' + info.cDay : '0' + info.cDay;
var lMonth = info.lMonth > 9 ? '' + info.lMonth : '0' + info.lMonth;
var lDay = info.lDay > 9 ? '' + info.lDay : '0' + info.lDay;
var code1 = cMonth + cDay;
var code2 = cMonth + Math.ceil(info.cDay / 7) + info.nWeek % 7;
var code3 = lMonth + lDay;
var days = getMonthDays(obj);
//节日信息
info['festival'] = '';
if (_festival3[code3]) {
info['festival'] += _festival3[code3];
}
if (_festival1[code1]) {
info['festival'] += ' ' + _festival1[code1];
}
if (_festival2[code2]) {
info['festival'] += ' ' + _festival2[code2];
}
if (obj['day'] + 7 > days) {
var code4 = cMonth + 5 + info.nWeek % 7;
if (code4 != code2 && _festival2[code4]) {
info['festival'] += ' ' + _festival2[code4];
}
}
info['festival'] = info['festival'].trim();
//放假、调休等标记
info['sign'] = '';
if (_holiday[info.cYear]) {
var holiday = _holiday[info.cYear];
if (typeof holiday[code1] != 'undefined') {
info['sign'] = holiday[code1] ? 'holiday' : 'work';
}
}
if (info.cYear == _today.year && info.cMonth == _today.month && info.cDay == _today.day) {
info['sign'] = 'today';
}
return info;
};
//获取日历信息
return (function (date) {
var date = date || _today;
var first = getDateObj(date['year'], date['month'], 1); //当月第一天
var days = getMonthDays(date); //当月天数
var data = []; //日历信息
var obj = {};
//上月日期
for (var i = first['week']; i > 0; i--) {
obj = getDateObj(first['year'], first['month'], first['day'] - i);
var info = getDateInfo(obj);
info['disabled'] = 1;
data.push(info);
}
//当月日期
for (var i = 0; i < days; i++) {
obj = {
'year': first['year'],
'month': first['month'],
'day': first['day'] + i,
'week': (first['week'] + i) % 7
};
var info = getDateInfo(obj);
info['disabled'] = 0;
data.push(info);
}
//下月日期
var last = obj;
for (var i = 1; last['week'] + i < 7; i++) {
obj = getDateObj(last['year'], last['month'], last['day'] + i);
var info = getDateInfo(obj);
info['disabled'] = 1;
data.push(info);
}
return {
'date': getDateInfo(date), //当前日历选中日期
'data': data
};
});
})();
var d = new Date();
var lunarInfo = getLunarData({
'year': d.getFullYear(),
'month': d.getMonth() + 1,
'day': d.getDate()
});
// console.log(JSON.stringify(lunarInfo, 2));
/*
"date": {
"lYear": 2022,
"lMonth": 1,
"lDay": 14,
"Animal": "虎",
"IMonthCn": "正月",
"IDayCn": "十四",
"cYear": 2022,
"cMonth": 2,
"cDay": 14,
"gzYear": "壬寅",
"gzMonth": "壬寅",
"gzDay": "戊戌",
"isToday": true,
"isLeap": false,
"nWeek": 1,
"ncWeek": "星期一",
"isTerm": false,
"Term": null,
"festival": "情人节",
"sign": "today"
},
*/
let today = lunarInfo.date;
let linearGradient = {
type: "linear",
colors: ["red", "yellow"],
startPoint: "top",
endPoint: "bottom",
};
$render(
<vstack
background={$gradient(linearGradient)}
frame="max,center"
>
<hstack>
<text font="title2">{today.Animal} 年</text>
<vstack>
<text font="title">{today.IMonthCn}</text>
<text font="title">{today.IDayCn}</text>
</vstack>
</hstack>
<text font="caption">{today.festival}</text>
</vstack>
);
Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/ and can be imported directly into the app.