Created
November 17, 2024 12:45
-
-
Save braziliany/6e2ae32fb58c04e07e5319ef525a8579 to your computer and use it in GitHub Desktop.
彩云天气小组件
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: deep-green; icon-glyph: cloud; | |
// Author:Pih | |
// 该小组件为在作者Pih的基础上进行的UI界面修改及相关代码调整Telegram @anker1209 | |
// 0427 - [1]加快了处理速度[2]减少了无意义的查询[3]减少报错 | |
// Update time:2021/04/27 | |
log("开始运行"); | |
//#region 初始化设定 | |
const files = FileManager.local(); //文件存储位置 | |
const currentDate = new Date(); | |
const apiKey = "UR8ASaplvIwavDfR"; //彩云天气apiKey,自行申请 | |
const isShowLocation = true; // 是否显示位置信息 | |
const tencentApiKey = ""; // 腾讯位置服务apiKey,自带官方key,也可以使用自己申请的 | |
const lockLocation = true; //是否锁定定位信息 | |
const lockLocationData = "浙江省"; // 锁定的定位信息,如果不填则从缓存中取 | |
const locale = "zh_cn"; //时间显示语言 | |
const newBG = false; //是否设置或者使用新的背景图片 | |
const removeBG = false; //是否需要清空背景图片 | |
const showYY = false; //是否只显示一言,否则优先显示日历事件 | |
const numberOfEvents = 3; //显示事件数量 | |
const targetDate = 360; //显示未来多少天内的日历事件 | |
const ignoreCal = "美国节假日"; //添加你需要不显示的事件的日历名称,多个日历用逗号分割,例如:"中国节假日", "工作", "生日" | |
//#endregion | |
if (numberOfEvents <= 0) showYY = true; | |
//#region 颜色设置 | |
const gColor = "#FFFFFF"; //全局颜色,如果想调整各部分颜色,修改下方相应颜色即可 | |
const dayColor = "3691be"; // 白天 天蓝色3691be | |
const nightColor = "23253c"; // 晚上 黑灰色23253c | |
const bgcolor = new Color(dayColor, 1); //背景颜色 | |
const timecolor = new Color(gColor, 1); //时间颜色 | |
const ctcolor = new Color(gColor, 1); //实时温度颜色 | |
const ctbgcolor = new Color(gColor, 0.2); //实时温度条背景颜色 | |
const ctfgcolor = new Color(gColor, 1); //实时温度条前景颜色 | |
const todaycolor = new Color(gColor, 1); //今日最低最高温度颜色 | |
const aircolor = new Color("000000", 0.8); //AQI空气质量颜色 | |
const dotcolor = new Color(gColor, 0.3); //虚线颜色 | |
const etitlecolor = new Color(gColor, 1); //事件标题颜色 | |
const etimecolor = new Color(gColor, 0.6); //事件时间颜色 | |
const hourlycolor = new Color(gColor, 1); //小时预报文字颜色 | |
const hourlybgcolor = new Color(gColor,0.2); //小时预报温度条背景颜色 | |
const hourlyfgcolor = new Color(gColor, 1); //小时预报温度条前景颜色 | |
const daybarcolor = new Color(gColor, 0.8); //星期预报分割条颜色 | |
const daycolor = new Color(gColor, 1); //星期预报文字颜色 | |
const rainycolor = new Color(gColor, 0); //下雨天温度字体颜色 | |
const yycolor = new Color(gColor, 1); //一言颜色 | |
const btcolor = new Color(gColor, 1); //下雨天温度字体颜色 | |
//#endregion | |
//#region 设备信息 | |
const scale = Device.screenScale(); | |
const widgetHeight = getWidgetSize().h; | |
const widgetWidth = getWidgetSize().w; | |
// 背景调整 | |
const firstRibbonPosition = 10; | |
const secondRibbonPosition = 71; | |
const defaultfontsize = 20; | |
const slipPosition = 370; | |
const daystoShow = 6; | |
//#endregion | |
//#region 天气信息 | |
const myLocation = await getLocation(); | |
if (!myLocation) throw "定位失败"; | |
// ######数据设定####### | |
const caiyun = await getCaiyunData(); | |
if (!caiyun || !caiyun.dataToday || !caiyun.dataToday.result) throw "未加载到天气数据"; | |
const alertInfo = caiyun.dataToday.result.alert.content[0]; | |
// log ("预警信息"+alertInfo) | |
const dailyTemperature = caiyun.dataToday.result.daily.temperature; | |
const rainIndex = caiyun.dataToday.result.hourly.precipitation; | |
const comfortindex = caiyun.dataToday.result.realtime.life_index.comfort.index; | |
const feelslikeT = Math.round(caiyun.dataToday.result.realtime.temperature); | |
const currentTemperature = feelslikeT.toString(); // +"º" | |
const feeling = caiyun.dataToday.result.realtime.life_index.comfort.desc; | |
const realtimeweather = caiyun.dataToday.result.realtime.skycon; | |
const todaysunrise = caiyun.dataToday.result.daily.astro[0].sunrise.time; | |
const todaysunset = caiyun.dataToday.result.daily.astro[0].sunset.time; | |
const todaysunsetdate = caiyun.dataToday.result.daily.astro[0].date; | |
const tomorrowsunrise = caiyun.dataToday.result.daily.astro[1].sunrise.time; | |
const tomorrowsunrisedate = caiyun.dataToday.result.daily.astro[1].date; | |
const tomorrowsunset = caiyun.dataToday.result.daily.astro[1].sunset.time; | |
const data = caiyun.dataToday.result.hourly.temperature; | |
const dailydata = caiyun.dataToday.result.daily.temperature; | |
const Mainweather = caiyun.dataToday.result.daily.skycon; | |
const CHNAQI = caiyun.dataToday.result.realtime.air_quality.aqi.chn.toString(); | |
const CHNAQIDes = caiyun.dataToday.result.realtime.air_quality.description.chn; | |
// log(CHNAQI) | |
const USAQI = caiyun.dataToday.result.realtime.air_quality.aqi.usa.toString(); | |
// log(USAQI) | |
const weatherDesc = caiyun.dataToday.result.forecast_keypoint; | |
const probabilityOfRain = caiyun.dataToday.result.minutely.probability; | |
// log(probabilityOfRain) | |
const maxProbability = | |
(Math.max(probabilityOfRain[0], probabilityOfRain[1], probabilityOfRain[2], probabilityOfRain[3]) * 100) | |
.toString() | |
.slice(0, 2) + "%"; | |
//#endregion | |
// ######字体设置####### | |
const widget = new ListWidget(); | |
widget.setPadding(0, 0, 0, 0); | |
//#region 根据日出日落时间更换背景色 | |
let dyBgColor = new Color(dayColor, 1); | |
let replaceStr = "00:00"; | |
let todaysunriseTime = new Date(todaysunsetdate.replace(replaceStr, todaysunrise)); | |
let todaySunsetTime = new Date(todaysunsetdate.replace(replaceStr, todaysunset)); | |
let tomorrowSunriseTime = new Date(tomorrowsunrisedate.replace(replaceStr, tomorrowsunrise)); | |
if (currentDate.getTime() > todaySunsetTime.getTime() && currentDate.getTime() > todaysunriseTime) { | |
dyBgColor = new Color(nightColor, 1); | |
} | |
if (currentDate.getTime() < todaySunsetTime.getTime() && currentDate.getTime() < todaysunriseTime) { | |
dyBgColor = new Color(nightColor, 1); | |
} | |
widget.backgroundColor = dyBgColor; | |
//#endregion | |
//#region 背景选择 | |
const imgPath = files.joinPath(files.documentsDirectory(), "testPath"); | |
if (newBG && config.runsInApp) { | |
const img = await Photos.fromLibrary(); | |
widget.backgroundImage = img; | |
files.writeImage(imgPath, img); | |
} else { | |
if (files.fileExists(imgPath)) { | |
try { | |
widget.backgroundImage = files.readImage(imgPath); | |
log("读取图片成功"); | |
} catch (e) { | |
widget.backgroundColor = bgcolor; | |
log(e.message); | |
} | |
} | |
} | |
if (removeBG && files.fileExists(imgPath)) { | |
try { | |
files.remove(imgPath); | |
log("背景图片清理成功"); | |
} catch (e) { | |
widget.backgroundColor = bgcolor; | |
log(e.message); | |
} | |
} | |
//#endregion | |
//#region 顶部虚线绘制 | |
const topDrawing = new DrawContext(); | |
topDrawing.size = new Size(642, secondRibbonPosition + 17); | |
topDrawing.opaque = false; | |
topDrawing.respectScreenScale = true; | |
for (i=0;i<78;i++) | |
{drawLine(topDrawing,30+i*7.5, 84, 35+i*7.5, 84, dotcolor, 2)} | |
//#endregion | |
//#region 空气质量颜色以及程度 | |
var AQIcolor; | |
// 选择 AQI 数据 USAQI or CHNAQI | |
let AQIData = USAQI; | |
if (AQIData <= 50) { | |
ac = "00e400"; | |
} else if (AQIData <= 100) { | |
ac = "f8c50a"; | |
} else if (AQIData <= 150) { | |
ac = "ff7e00"; | |
} else if (AQIData <= 200) { | |
ac = "ff0000"; | |
} else if (AQIData <= 300) { | |
ac = "ba0033"; | |
} else { | |
ac = "7e0023"; | |
} | |
AQIcolor = new Color(ac, 1); | |
fillRect(topDrawing, 522, 58, 90, 18, 6, AQIcolor); | |
//#endregion | |
//#region 位置信息 | |
if (isShowLocation) { | |
const areaData = await getLocationArea(); | |
if (areaData && areaData.result && areaData.result.address_component) { | |
const city = areaData.result.address_component.city; | |
const district = areaData.result.address_component.district; | |
const street = areaData.result.address_component.street; | |
//drawText(topDrawing,30, 28, 150, 24, city, timecolor, "regular",20,"left") | |
drawText(topDrawing, 30, 28, 150, 24, district, timecolor, "regular", 20, "left"); | |
drawText(topDrawing, 30, 54, 150, 24, street, timecolor, "regular", 20, "left"); | |
} | |
} | |
//#endregion | |
//#region 日期显示 | |
const df = new DateFormatter(); | |
df.locale = locale; | |
const date = currentDate; | |
// 获取周数 | |
let beginDate = new Date(date.getFullYear(), 0, 1); | |
let theWeek = | |
"第" + Math.ceil((parseInt((date - beginDate) / (24 * 60 * 60 * 1000)) + 1 + beginDate.getDay()) / 7) + "周"; | |
log("开始时间" + beginDate); | |
log("计算结果" + Math.ceil((parseInt((date - beginDate) / (24 * 60 * 60 * 1000)) + 1 + beginDate.getDay()) / 7)); | |
log(theWeek); | |
// 星期 EEEE星期几 EEE周几 | |
df.dateFormat = "EEEE"; | |
let week = df.string(date); | |
// 日期格式 | |
df.dateFormat = "MMMd日"; | |
let dateFormat = df.string(date); | |
// 获取农历信息 | |
const lunarInfo = await getLunar(date.getDate() - 1); | |
let lunarJoinInfo = lunarInfo.infoLunarText + " " + lunarInfo.holidayText; | |
let datePointX = isShowLocation ? 140 : 30; | |
drawText( | |
topDrawing, | |
datePointX, | |
28, | |
250, | |
24, | |
currentDate.getFullYear().toString().concat("年").concat(dateFormat) + " " + theWeek, | |
timecolor, | |
"regular", | |
20, | |
"left" | |
); | |
drawText(topDrawing, datePointX, 54, 300, 24, week + " " + lunarJoinInfo, timecolor, "regular", 20, "left"); | |
// drawText(topDrawing,160, firstRibbonPosition+40, 225, 24, lunarJoinInfo, timecolor, "regular",20,"left") | |
//#endregion | |
//#region 当天天气信息 | |
// 当前天气 | |
await drawIcon(topDrawing, 365, 30, realtimeweather, 40); | |
// 当前温度 | |
drawText(topDrawing, 420, 25, 100, 54, currentTemperature, ctcolor, "regular", 52, "center"); | |
// 空气质量&下雨概率 | |
var textColortoShow; | |
textColortoShow = aircolor; | |
// 显示长度截取 | |
let des = CHNAQIDes; | |
des = des.length > 2 ? des.slice(0, 2) : des; | |
drawText(topDrawing, 522, 58, 90, 17, AQIData + " - " + des, textColortoShow, "semibold", 15, "center"); | |
//#region 温度条位置 | |
var tempHeight; | |
if (feelslikeT < Math.round(dailyTemperature[0].min)) { | |
tempHeight = 8; | |
} | |
if (feelslikeT > Math.round(dailyTemperature[0].max)) { | |
tempHeight = 90; | |
} | |
if (feelslikeT >= Math.round(dailyTemperature[0].min) && feelslikeT <= Math.round(dailyTemperature[0].max)) { | |
tempHeight = | |
((feelslikeT - Math.round(dailyTemperature[0].min)) * 82) / | |
(Math.round(dailyTemperature[0].max) - Math.round(dailyTemperature[0].min)) + | |
8; | |
} | |
//#endregion | |
//#region 温度条 | |
fillRect(topDrawing, 522, 47, 90, 8, 4, ctbgcolor); | |
fillRect(topDrawing, 522, 47, tempHeight, 8, 4, ctfgcolor); | |
//#endregion | |
// 今天最高最低温度 +"º" | |
drawText( | |
topDrawing, | |
522, | |
25, | |
45, | |
18, | |
Math.round(dailyTemperature[0].min).toString(), | |
todaycolor, | |
"semibold", | |
18, | |
"left" | |
); | |
drawText( | |
topDrawing, | |
566, | |
25, | |
45, | |
18, | |
Math.round(dailyTemperature[0].max).toString(), | |
todaycolor, | |
"semibold", | |
18, | |
"right" | |
); | |
const contentStack = widget.addStack(); | |
contentStack.layoutVertically(); | |
contentStack.size = new Size(widgetWidth / scale, widgetHeight / scale); | |
const topStack = contentStack.addStack(); | |
topStack.size = new Size(widgetWidth / scale, (widgetHeight * 98) / 296 / scale); | |
topStack.addImage(topDrawing.getImage()); | |
// contentStack.url = 'calshow:' + (Math.floor(currentDate.getTime() / 1000) - 978307200) | |
//#endregion | |
//#region 获取日程和一言 | |
const middleStack = contentStack.addStack(); | |
middleStack.layoutHorizontally(); | |
const eventStack = middleStack.addStack(); | |
eventStack.size = new Size((widgetWidth * 355) / 642 / scale, (widgetHeight * (255 - 88)) / 296 / scale); | |
if (showYY) { | |
eventStack.layoutVertically(); | |
eventStack.setPadding(0, 16, 0, 10); | |
eventStack.addSpacer(); | |
const providepoetry = await poetry(); | |
let yy = providepoetry.hitokoto; | |
let yyshow = eventStack.addText(yy); | |
yyshow.font = Font.lightSystemFont(14); | |
yyshow.textColor = yycolor; | |
eventStack.addSpacer(); | |
const r1 = eventStack.addStack(); | |
r1.addSpacer(); | |
let author_who = r1.addText(providepoetry.from_who + " " + providepoetry.from); | |
author_who.font = Font.lightSystemFont(10); | |
author_who.textColor = yycolor; | |
author_who.textOpacity = 0.8; | |
author_who.linelimit = 1; | |
eventStack.addSpacer(6); | |
} else { | |
const eventDrawing = new DrawContext(); | |
eventDrawing.size = new Size(355, 255 - 98); | |
eventDrawing.opaque = false; | |
eventDrawing.respectScreenScale = true; | |
currentDate.setDate(currentDate.getDate()); | |
console.log(`Filter event by start date ${currentDate}`); | |
// 结束时间设置为当日"+targetDate"天的日期 | |
const endDate = new Date(); | |
endDate.setDate(endDate.getDate() + targetDate); | |
console.log(`Filter event by end date ${endDate}`); | |
const allEvents = await CalendarEvent.between(currentDate, endDate, []); | |
console.log(`Get ${allEvents.length} events from this time range`); | |
const futureEvents = showYY ? [] : enumerateEvents(allEvents); | |
for (let i = 0; i < numberOfEvents; i++) { | |
const event = futureEvents[i]; | |
if (!event) { | |
break; | |
} | |
const eventColor = event.calendar.color; | |
fillRect(eventDrawing, 30, 88 - 88 + i * 55, 5, 45, 2, eventColor); | |
// 标题 | |
const title = event.title; | |
drawText(eventDrawing, 45, 86 - 88 + i * 55, 305, 24, title, etitlecolor, "bold", 22, "left"); | |
// 限制行高。 | |
if (futureEvents.length >= 3) { | |
title.lineLimit = 1; | |
} | |
// 格式化时间信息。 | |
let df = new DateFormatter(); | |
df.useShortDateStyle(); | |
// 剩余时间(日) | |
const timeLeft = | |
(df.date(df.string(event.startDate)) - df.date(df.string(currentDate))) / (1000 * 60 * 60 * 24); | |
// const eventSeconds = Math.floor(currentDate.getTime() / 1000) - 978307200 + timeLeft * 3600 * 24; | |
// const duration = (df.date(df.string(event.endDate)) - df.date(df.string(event.startDate))) / (1000 * 60 * 60 * 24); //事件持续时间 | |
// log(timeLeft) | |
// 事件时间提醒 | |
var timeText; | |
var eventTimeColor; | |
if (timeLeft == 0) { | |
df.useNoDateStyle(); | |
df.useShortTimeStyle(); | |
eventTimeColor = new Color("ffff00"); | |
if (event.isAllDay) { | |
timeText = "今天全天"; | |
} else { | |
timeText = "今天" + df.string(event.startDate); | |
console.log(`${df.date(df.string(event.startDate))}`); | |
} | |
} | |
// } else { | |
if ( | |
timeLeft < 0 | |
// && duration > 0 | |
) { | |
let df = new DateFormatter(); | |
const longEventDate = "d日HH:mm"; | |
df.dateFormat = longEventDate; | |
timeText = df.string(event.startDate) + "-" + df.string(event.endDate); | |
// 今日事件字体颜色 | |
eventTimeColor = new Color("ffff00"); | |
} | |
// else { | |
if (timeLeft == 1) { | |
df.useNoDateStyle(); | |
df.useShortTimeStyle(); | |
if (event.isAllDay) { | |
timeText = "明天全天"; | |
eventTimeColor = new Color("ffff00"); | |
} else { | |
timeText = "明天" + df.string(event.startDate); | |
eventTimeColor = new Color("ffff00"); | |
} | |
} | |
// else{ | |
if (timeLeft > 1) { | |
let df = new DateFormatter(); | |
let startTime = event.startDate; | |
let finishTime = event.endDate; | |
df.dateFormat = "EEE"; | |
let eee = df.string(startTime); | |
df.dateFormat = "MMMd日"; | |
let ddd = df.string(startTime); | |
let sep = "-"; | |
df.dateFormat = "h:mm"; | |
let detailTimes = df.string(startTime) + sep + df.string(finishTime); | |
if (event.isAllDay) { | |
timeText = eee + " " + ddd + " 在" + timeLeft + "天之后"; | |
} else { | |
timeText = eee + " " + ddd + " " + detailTimes + " 在" + timeLeft + "天之后"; | |
} | |
eventTimeColor = etimecolor; | |
// log(timeText) | |
} | |
// log (timeText) | |
drawText(eventDrawing, 45, 88 - 88 + i * 55 + 25, 305, 30, timeText, eventTimeColor, "medium", 18, "left"); | |
} | |
eventStack.addImage(eventDrawing.getImage()); | |
} | |
eventStack.url = "calshow:" + (Math.floor(currentDate.getTime() / 1000) - 978307200); | |
//#endregion | |
//#region 天气预报 | |
const weatherDrawing = new DrawContext(); | |
weatherDrawing.size = new Size(642 - 355, 255 - 98); | |
weatherDrawing.opaque = false; | |
weatherDrawing.respectScreenScale = true; | |
const deltaX = (610 - slipPosition) / (daystoShow * 2); | |
const firstPointtoLeft = slipPosition + deltaX; | |
const ToTop = 120 - 98; | |
var min, max, diff; | |
for (var i = 0; i < daystoShow; i++) { | |
let temp = Math.round(data[i].value); | |
min = temp < min || min == undefined ? temp : min; | |
max = temp > max || max == undefined ? temp : max; | |
} | |
diff = max - min; | |
if (diff == 0) { | |
diff = diff + 1; | |
max = max + 0.3; | |
} | |
for (i = 0; i < daystoShow - 1; i++) { | |
let timeText = data[i].datetime.slice(11, 13); | |
} | |
//#region 小时预报 | |
for (i = 0; i < daystoShow; i++) { | |
// 颜色定义 | |
var temperaturetextcolor; | |
var temeratureBarcolor; | |
if (rainIndex[i * 2].value >= 0.06) { | |
temperaturetextcolor = rainycolor; | |
temeratureBarcolor = rainycolor; | |
} else { | |
temperaturetextcolor = hourlycolor; | |
temeratureBarcolor = hourlyfgcolor; | |
} | |
// 温度条位置 | |
if (Math.round(data[i * 2].value) < Math.round(dailyTemperature[0].min)) { | |
tempHeight = 8; | |
} | |
if (Math.round(data[i * 2].value) >= Math.round(dailyTemperature[0].max)) { | |
tempHeight = 40; | |
} | |
if ( | |
Math.round(data[i * 2].value) >= Math.round(dailyTemperature[0].min) && | |
Math.round(data[i * 2].value) < Math.round(dailyTemperature[0].max) | |
) | |
tempHeight = | |
((Math.round(data[i * 2].value) - Math.round(dailyTemperature[0].min)) * 32) / | |
(Math.round(dailyTemperature[0].max) - Math.round(dailyTemperature[0].min)) + | |
8; | |
// ######温度条####### | |
fillRect(weatherDrawing, firstPointtoLeft - 4 + 2.15 * i * deltaX - 355 - 5, 115 - 98 + 3, 8, 40, 4, hourlybgcolor); | |
fillRect( | |
weatherDrawing, | |
firstPointtoLeft - 4 + 2.15 * i * deltaX - 355 - 5, | |
150 - tempHeight - 98 + 3 + 5, | |
8, | |
tempHeight, | |
4, | |
temeratureBarcolor | |
); | |
// 温度+"º" | |
drawText( | |
weatherDrawing, | |
firstPointtoLeft + deltaX * i * 2.15 - 20 - 355 - 5, | |
150 - 98 + 10, | |
40, | |
20, | |
Math.round(data[i * 2].value).toString(), | |
temperaturetextcolor, | |
"regular", | |
16, | |
"center" | |
); | |
// 时间 | |
let weathertimeText = data[i * 2].datetime.slice(11, 13); | |
let zero = weathertimeText.slice(0, 1); | |
weathertimeText = zero == 0 ? weathertimeText.replace("0", "") : weathertimeText; | |
if (i == 0) { | |
weathertimeText = "现在"; | |
} else { | |
weathertimeText = weathertimeText + "时"; | |
// + ":00" | |
} | |
drawText( | |
weatherDrawing, | |
firstPointtoLeft + deltaX * i * 2.1 - 20 - 355 - 5, | |
100 - 98 - 4, | |
40, | |
30, | |
weathertimeText, | |
temperaturetextcolor, | |
"regular", | |
16, | |
"center" | |
); | |
} | |
//#endregion | |
//#region 每日预报 | |
for (i = 1; i < 4; i++) { | |
// log(Mainweather[i].value) | |
// 图标 | |
await drawIcon(weatherDrawing, 10 + 88 * (i - 1) + 40 - 1 - 3, 196 - 98 - 10 + 3, Mainweather[i].value, 30); | |
// 每日温度+"º" | |
let dMax = Math.round(dailydata[i].max).toString(); | |
let dMin = Math.round(dailydata[i].min).toString(); | |
fillRect(weatherDrawing, 5 + 88 * (i - 1) + 6 + 1, 173 - 98 + 40 + 16 + 2, 70, 4, 2, daybarcolor); | |
drawText( | |
weatherDrawing, | |
5 + 88 * (i - 1) + 6 + 1, | |
173 - 98 + 20 + 62 - 18, | |
40, | |
20, | |
dMin, | |
daycolor, | |
"regular", | |
18, | |
"left" | |
); | |
drawText( | |
weatherDrawing, | |
5 + 88 * (i - 1) + 6 + 40 + 1, | |
173 - 98 + 20 + 62 - 18, | |
30, | |
20, | |
dMax, | |
daycolor, | |
"regular", | |
18, | |
"right" | |
); | |
// 每日日期 | |
const weatherDate = new Date(); | |
weatherDate.setDate(weatherDate.getDate() + i); | |
// log(weatherDate) | |
df.dateFormat = "E"; | |
drawText( | |
weatherDrawing, | |
5 + 88 * (i - 1) - 1, | |
173 - 98 + 24, | |
50, | |
20, | |
df.string(weatherDate), | |
daycolor, | |
"a", | |
16, | |
"center" | |
); | |
} | |
const weatherStack = middleStack.addStack(); | |
weatherStack.size = new Size((widgetWidth * (642 - 355)) / 642 / scale, (widgetHeight * (255 - 98)) / 296 / scale); | |
weatherStack.addImage(weatherDrawing.getImage()); | |
weatherStack.url = "https://caiyunapp.com/weather/"; | |
//#endregion | |
//#endregion | |
//#region 底部信息展示 | |
const bottomDrawing = new DrawContext(); | |
bottomDrawing.size = new Size(642, 296 - 255); | |
bottomDrawing.opaque = false; | |
bottomDrawing.respectScreenScale = true; | |
// 底部虚线绘制 | |
for (i=0;i<78;i++) | |
{drawLine(bottomDrawing,30+i*7.5, 1, 35+i*7.5, 1,dotcolor, 2)} | |
// 如果没有预警信息,显示天气描述 | |
var content; | |
var alertTextColor; | |
// log (alertInfo) | |
if (alertInfo == undefined) { | |
content = weatherDesc; | |
} else { | |
content = "注意:" + alertInfo.title; | |
} | |
drawText(bottomDrawing, 0, 7, 642, 25, content, btcolor, "regular", 20, "center"); | |
const bottomStack = contentStack.addStack(); | |
bottomStack.size = new Size(widgetWidth / scale, (widgetHeight * (296 - 255)) / 296 / scale); | |
bottomStack.addImage(bottomDrawing.getImage()); | |
//#endregion | |
Script.setWidget(widget); | |
widget.presentMedium(); | |
Script.complete(); | |
//#region 绘画帮助 | |
/** 画线 */ | |
function drawLine(drawing, x1, y1, x2, y2, color, width) { | |
let path = new Path(); | |
path.move(new Point(Math.round(x1), Math.round(y1))); | |
path.addLine(new Point(Math.round(x2), Math.round(y2))); | |
drawing.addPath(path); | |
drawing.setStrokeColor(color); | |
drawing.setLineWidth(width); | |
drawing.strokePath(); | |
} | |
/** 绘制文字 */ | |
function drawText(drawing, x, y, width, height, text, color, font, fontsize, alignment) { | |
if (font == "a") { | |
drawing.setFont(Font.boldRoundedSystemFont(fontsize)); | |
} | |
if (font == "default") { | |
drawing.setFont(Font.lightMonospacedSystemFont(fontsize)); | |
} | |
if (font == "semibold") { | |
drawing.setFont(Font.semiboldSystemFont(fontsize)); | |
} | |
if (font == "bold") { | |
drawing.setFont(Font.boldSystemFont(fontsize)); | |
} | |
if (font == "medium") { | |
drawing.setFont(Font.mediumSystemFont(fontsize)); | |
} | |
if (font == "regular") { | |
drawing.setFont(Font.regularSystemFont(fontsize)); | |
} | |
drawing.setTextColor(color); | |
if (alignment == "left") { | |
drawing.setTextAlignedLeft(); | |
} | |
if (alignment == "center") { | |
drawing.setTextAlignedCenter(); | |
} | |
if (alignment == "right") { | |
drawing.setTextAlignedRight(); | |
} | |
drawing.drawTextInRect(text, new Rect(x, y, width, height)); | |
} | |
/** 绘制主要天气图标 */ | |
async function drawIcon(drawing, x1, y1, WeatherCondition, size) { | |
if (WeatherCondition == "CLOUDY") { | |
y1 = y1 + 8; | |
} | |
if ( | |
WeatherCondition == "LIGHT_RAIN" || | |
WeatherCondition == "MODERATE_RAIN" || | |
WeatherCondition == "HEAVY_RAIN" || | |
WeatherCondition == "STORM_RAIN" | |
) { | |
y1 = y1 + 4; | |
} | |
await drawSfs(drawing, x1, y1, provideSymbol(WeatherCondition, 0), size, "FFFFFF"); | |
} | |
/** 提供天气图标名称 */ | |
function provideSymbol(cond, night) { | |
// log("字体大小"+size) | |
let symbols = { | |
CLEAR_DAY: function () { | |
return "sun.max.fill"; | |
}, | |
CLEAR_NIGHT: function () { | |
return "moon.stars.fill"; | |
}, | |
PARTLY_CLOUDY_DAY: function () { | |
return "cloud.sun.fill"; | |
}, | |
PARTLY_CLOUDY_NIGHT: function () { | |
return "cloud.moon.fill"; | |
}, | |
CLOUDY: function () { | |
return "cloud.fill"; | |
}, | |
LIGHT_HAZE: function () { | |
return night ? "cloud.fog.fill" : "sun.haze.fill"; | |
}, | |
MODERATE_HAZE: function () { | |
return night ? "cloud.fog.fill" : "sun.haze.fill"; | |
}, | |
HEAVY_HAZE: function () { | |
return night ? "cloud.fog.fill" : "sun.haze.fill"; | |
}, | |
LIGHT_RAIN: function () { | |
return "cloud.drizzle.fill"; | |
}, | |
MODERATE_RAIN: function () { | |
return "cloud.rain.fill"; | |
}, | |
HEAVY_RAIN: function () { | |
return "cloud.rain.fill"; | |
}, | |
STORM_RAIN: function () { | |
return "cloud.heavyrain.fill"; | |
}, | |
FOG: function () { | |
return "cloud.fog.fill"; | |
}, | |
LIGHT_SNOW: function () { | |
return "cloud.sleet.fill"; | |
}, | |
MODERATE_SNOW: function () { | |
return "cloud.snow.fill"; | |
}, | |
HEAVY_SNOW: function () { | |
return "cloud.snow.fill"; | |
}, | |
STORM_SNOW: function () { | |
return "snow"; | |
}, | |
DUST: function () { | |
return night ? "cloud.fog.fill" : "sun.dust.fill"; | |
}, | |
SAND: function () { | |
return night ? "cloud.fog.fill" : "sun.dust.fill"; | |
}, | |
WIND: function () { | |
return "wind"; | |
}, | |
}; | |
return symbols[cond](); | |
} | |
/** 绘制symblos */ | |
async function drawSfs(drawing, x1, y1, symblos, size, color) { | |
let sfs = SFSymbol.named(symblos); | |
sfs.applyFont(Font.systemFont(size)); | |
let img = sfs.image; | |
let col = new Color(color ? color : "FFFFFF"); | |
let tintedSymbol = await tintSFSymbol(img, col); | |
let symbolCanvas = new DrawContext(); | |
symbolCanvas.opaque = false; | |
symbolCanvas.size = new Size(size, size); | |
const symbolRect = new Rect(0, 0, size, size); | |
symbolCanvas.drawImageInRect(tintedSymbol, symbolRect); | |
const newSymbolImage = symbolCanvas.getImage(); | |
drawing.drawImageAtPoint(newSymbolImage, new Point(x1, y1)); | |
} | |
function fillRect(drawing, x, y, width, height, cornerradio, color) { | |
let path = new Path(); | |
let rect = new Rect(x, y, width, height); | |
path.addRoundedRect(rect, cornerradio, cornerradio); | |
drawing.addPath(path); | |
drawing.setFillColor(color); | |
drawing.fillPath(); | |
} | |
function drawPoint(drawing, x1, y1, color, diaofPoint) { | |
let currPath = new Path(); | |
currPath.addEllipse(new Rect(x1, y1, diaofPoint, diaofPoint)); | |
drawing.addPath(currPath); | |
drawing.setFillColor(color); | |
drawing.fillPath(); | |
} | |
//#endregion | |
//#region 数据获取 | |
/** 获取彩云天气数据# */ | |
async function getCaiyunData() { | |
var data; | |
var cacheValue = cacheGet("CaiyunCache", 120); | |
// 假设存储器已经存在且距离上次请求时间不足60秒,使用存储器数据 | |
if (cacheValue) { | |
log("==>请求时间间隔过小,使用缓存数据"); | |
data = jsonParse(cacheValue); | |
// 否则利用 api 得到新的数据 | |
} else { | |
try { | |
const weatherReq = | |
"https://api.caiyunapp.com/v2.5/" + | |
apiKey + | |
"/" + | |
myLocation + | |
"/weather.json?alert=true&dailysteps=7&lang=zh_CN"; | |
const dataToday = await new Request(weatherReq).loadJSON(); | |
log(JSON.stringify(weatherReq)); | |
data = { dataToday }; | |
cacheSet("CaiyunCache", JSON.stringify(data)); | |
log("==>天气信息请求成功"); | |
} catch (e) { | |
data = jsonParse(cacheGet("CaiyunCache"), {}); | |
log("==>天气信息请求失败,使用缓存数据/" + e.message); | |
} | |
} | |
return data; | |
} | |
/** 获取定位信息 */ | |
async function getLocation() { | |
var latitude, longitude; | |
var locationString; | |
// 如果位置设定保存且锁定了,从缓存文件读取信息 | |
var locationCache = cacheGet("mylocation", 120); | |
if (lockLocation) { | |
log("位置锁定,使用预设或缓存数据" + locationString); | |
locationString = lockLocationData || locationCache; | |
} else if (locationCache) { | |
log("位置查询过于频繁,使用缓存数据" + locationCache); | |
locationString = locationCache; | |
} else { | |
try { | |
const location = await Location.current(); | |
latitude = location.latitude; | |
longitude = location.longitude; | |
locationString = longitude + "," + latitude; | |
cacheSet("mylocation", locationString); | |
log("==>定位成功"); | |
} catch (e) { | |
locationString = cacheGet("mylocation"); | |
log("==>无法定位,使用缓存定位数据/" + e.message); | |
} | |
// return locationString | |
} | |
log("地址" + locationString); | |
return locationString; | |
} | |
/** 根据定位信息获取位置信息 | |
* location 经纬度 | |
* key API key | |
* get_poi 位置周边数据 0否 1是 | |
*/ | |
async function getLocationArea() { | |
let area; | |
// 加个缓存 | |
let areaCache = cacheGet("areaCache", 120); | |
// 假设存储器已经存在且距离上次请求时间不足60秒,使用存储器数据 | |
if (areaCache) { | |
log("腾讯位置API请求时间间隔过小,使用缓存数据"); | |
area = jsonParse(areaCache); | |
} | |
if (area) return area; | |
let locationLs = myLocation.split(","); | |
try { | |
// 官方文档的key | |
let testKey = "OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77"; | |
let apiKey = tencentApiKey == "" ? testKey : tencentApiKey; | |
let areaReq = | |
"https://apis.map.qq.com/ws/geocoder/v1?location=" + | |
locationLs[1] + | |
"," + | |
locationLs[0] + | |
"&key=" + | |
apiKey + | |
"&get_poi=0"; | |
let areaRequest = new Request(areaReq); | |
let header = { Referer: "https://lbs.qq.com/" }; | |
areaRequest.method = "GET"; | |
areaRequest.headers = header; | |
area = await areaRequest.loadJSON(); | |
log("腾讯位置API请求成功:" + areaReq); | |
cacheSet("areaCache", JSON.stringify(area)); | |
} catch (err) { | |
log("getLocationArea出错:" + err.message); | |
area = jsonParse(cacheGet("areaCache")); | |
} | |
return area; | |
} | |
/** 未来事件 */ | |
function enumerateEvents(allEvents) { | |
let futureEvents = []; | |
for (const event of allEvents) { | |
if ( | |
event.endDate.getTime() > currentDate.getTime() && | |
!event.title.startsWith("Canceled:") && | |
![ignoreCal].includes(event.calendar.title) | |
) { | |
futureEvents.push(event); | |
} | |
} | |
return futureEvents; | |
} | |
/** 获取一言 */ | |
async function poetry() { | |
var poetryData; | |
try { | |
poetryData = await new Request("https://v1.hitokoto.cn/?c=i&encode=json&max_length=40").loadJSON(); | |
cacheSet("yiyan", JSON.stringify(poetryData)); | |
log("==>一言获取成功"); | |
} catch (e) { | |
poetryData = jsonParse(cacheGet("yiyan")); | |
log("==>获取一言失败,使用缓存数据"); | |
} | |
return poetryData; | |
} | |
/** 获取万年历 */ | |
async function getLunar(day) { | |
// 缓存key | |
const cacheKey = "lsp-lunar-cache"; | |
// 万年历数据 | |
let response = undefined; | |
try { | |
const request = new Request("https://wannianrili.51240.com/"); | |
const defaultHeaders = { | |
"user-agent": | |
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36", | |
}; | |
request.method = "GET"; | |
request.headers = defaultHeaders; | |
const html = await request.loadString(); | |
let webview = new WebView(); | |
await webview.loadHTML(html); | |
var getData = ` | |
function getData() { | |
try { | |
infoLunarText = document.querySelector('div#wnrl_k_you_id_${day}.wnrl_k_you .wnrl_k_you_id_wnrl_nongli').innerText | |
holidayText = document.querySelectorAll('div.wnrl_k_zuo div.wnrl_riqi')[${day}].querySelector('.wnrl_td_bzl').innerText | |
if(infoLunarText.search(holidayText) != -1) { | |
holidayText = '' | |
} | |
} catch { | |
holidayText = '' | |
} | |
return {infoLunarText: infoLunarText, holidayText: holidayText} | |
} | |
getData()`; | |
// 节日数据 | |
response = await webview.evaluateJavaScript(getData, false); | |
cacheSet(cacheKey, JSON.stringify(response)); | |
console.log(`农历输出:${JSON.stringify(response)}`); | |
} catch (e) { | |
console.error(`农历请求出错:${e}`); | |
if (Keychain.contains(cacheKey)) { | |
response = jsonParse(cacheGet(cacheKey)); | |
} | |
} | |
return response; | |
} | |
//#endregion | |
//#region 帮助类 | |
function getWidgetSize() { | |
let deviceSize = (Device.screenSize().height * scale).toString(); | |
let deviceInfo = { | |
2796: { | |
models: ["14 Pro Max"], | |
small: { w: 510, h: 510 }, | |
medium: { w: 1092, h: 510 }, | |
large: { w: 1092, h: 1146 }, | |
}, | |
2556: { | |
models: ["14 Pro"], | |
small: { w: 474, h: 474 }, | |
medium: { w: 1014, h: 474 }, | |
large: { w: 1014, h: 1062 }, | |
}, | |
2778: { | |
models: ["12 Pro Max"], | |
small: { w: 510, h: 510 }, | |
medium: { w: 1092, h: 510 }, | |
large: { w: 1092, h: 1146 }, | |
}, | |
2532: { | |
models: ["12", "12 Pro"], | |
small: { w: 474, h: 474 }, | |
medium: { w: 1014, h: 474 }, | |
large: { w: 1014, h: 1062 }, | |
}, | |
2688: { | |
models: ["Xs Max", "11 Pro Max"], | |
small: { w: 507, h: 507 }, | |
medium: { w: 1080, h: 507 }, | |
large: { w: 1080, h: 1137 }, | |
}, | |
1792: { | |
models: ["11", "Xr"], | |
small: { w: 338, h: 338 }, | |
medium: { w: 720, h: 338 }, | |
large: { w: 720, h: 758 }, | |
}, | |
2436: { | |
models: ["X", "Xs", "11 Pro"], | |
small: { w: 465, h: 465 }, | |
medium: { w: 987, h: 465 }, | |
large: { w: 987, h: 1035 }, | |
}, | |
2208: { | |
models: ["6+", "6s+", "7+", "8+"], | |
small: { w: 471, h: 471 }, | |
medium: { w: 1044, h: 471 }, | |
large: { w: 1044, h: 1071 }, | |
}, | |
1334: { | |
models: ["6", "6s", "7", "8"], | |
small: { w: 296, h: 296 }, | |
medium: { w: 642, h: 296 }, | |
large: { w: 642, h: 648 }, | |
}, | |
1136: { | |
models: ["5", "5s", "5c", "SE"], | |
small: { w: 282, h: 282 }, | |
medium: { w: 584, h: 282 }, | |
large: { w: 584, h: 622 }, | |
}, | |
}; | |
let widgetSize = deviceInfo[deviceSize].medium; | |
return widgetSize; | |
} | |
/** json处理,防止报错 */ | |
function jsonParse(str, dft) { | |
if (typeof str == "string") { | |
try { | |
return JSON.parse(str); | |
} catch (e) { | |
if (dft) return dft; | |
return undefined; | |
} | |
} | |
return undefined; | |
} | |
/** 获取缓存数据 */ | |
function cacheGet(key, allowSeconds) { | |
let cache = ""; | |
if (!Keychain.contains(key)) return cache; | |
var json = jsonParse(Keychain.get(key)); | |
if (allowSeconds && allowSeconds > 0) { | |
if (currentDate - json.time <= allowSeconds * 1000) cache = json.value; | |
} else { | |
cache = json.value; | |
} | |
return cache; | |
} | |
/** 设置缓存数据 | |
* @param {String} key key | |
* @param {String} value value | |
*/ | |
function cacheSet(key, value) { | |
if (value == null || value == undefined) return; | |
if (typeof value != "string") value = JSON.stringify(value); | |
Keychain.set( | |
key, | |
JSON.stringify({ | |
value: value, | |
time: new Date().getTime(), | |
}) | |
); | |
} | |
/** 删除缓存数据 */ | |
function cacheDel(key) { | |
Keychain.remove(key); | |
} | |
/** | |
* source: https://talk.automators.fm/t/define-the-color-of-a-sf-symbols-in-drawcontext/9897/3 | |
* @param {Image} image The image from the SFSymbol | |
* @param {Color} color The color it should be tinted with | |
*/ | |
async function tintSFSymbol(image, color) { | |
let html = ` | |
<img id="image" src="data:image/png;base64,${Data.fromPNG(image).toBase64String()}" /> | |
<canvas id="canvas"></canvas> | |
`; | |
let js = ` | |
let img = document.getElementById("image"); | |
let canvas = document.getElementById("canvas"); | |
let color = 0x${color.hex}; | |
canvas.width = img.width; | |
canvas.height = img.height; | |
let ctx = canvas.getContext("2d"); | |
ctx.drawImage(img, 0, 0); | |
let imgData = ctx.getImageData(0, 0, img.width, img.height); | |
// ordered in RGBA format | |
let data = imgData.data; | |
for (let i = 0; i < data.length; i++) { | |
// skip alpha channel | |
if (i % 4 === 3) continue; | |
// bit shift the color value to get the correct channel | |
data[i] = (color >> (2 - i % 4) * 8) & 0xFF | |
} | |
ctx.putImageData(imgData, 0, 0); | |
canvas.toDataURL("image/png").replace(/^data:image\\/png;base64,/, ""); | |
`; | |
let wv = new WebView(); | |
await wv.loadHTML(html); | |
let base64 = await wv.evaluateJavaScript(js); | |
return Image.fromData(Data.fromBase64String(base64)); | |
} | |
//#endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment