mirror of
https://github.com/ThisIsBenny/iOS-Widgets.git
synced 2025-04-19 15:27:40 +00:00
579 lines
17 KiB
JavaScript
579 lines
17 KiB
JavaScript
// Variables used by Scriptable.
|
||
// These must be at the very top of the file. Do not edit.
|
||
// icon-color: deep-blue; icon-glyph: shopping-cart;
|
||
// Version 1.2.1
|
||
|
||
/// Used by enums
|
||
const enumValue = (name) => Object.freeze({toString: () => name})
|
||
|
||
/// Used by DisplayMode
|
||
|
||
const lightBackgroundColor = Color.white()
|
||
const darkBackgroundColor = new Color('#222', 1.0)
|
||
const autoBackgroundColor = Color.dynamic(lightBackgroundColor, darkBackgroundColor)
|
||
|
||
const lightTextColor = Color.black()
|
||
const darkTextColor = Color.white()
|
||
const autoTextColor = Color.dynamic(lightTextColor, darkTextColor)
|
||
|
||
const lightBackgroundProgressColor = new Color('#D2D2D7', 1.0)
|
||
const darkBackgroundProgressColor = new Color('#707070', 1.0)
|
||
const autoBackgroundProgressColor = Color.dynamic(lightBackgroundProgressColor, darkBackgroundProgressColor)
|
||
|
||
const lightFillProgressColor = new Color('#008009', 1.0)
|
||
const darkFillProgressColor = new Color('#00A00D', 1.0)
|
||
const autoFillProgressColor = Color.dynamic(lightFillProgressColor, darkFillProgressColor)
|
||
|
||
/**
|
||
* Enum for display mode.
|
||
* @readonly
|
||
* @enum {{name: string, backgroundColor: Color, textColor: Color}}
|
||
*/
|
||
const DisplayMode = Object.freeze({
|
||
LIGHT: {
|
||
name: "light",
|
||
backgroundColor: lightBackgroundColor,
|
||
textColor: lightTextColor,
|
||
backgroundProgressColor: lightBackgroundProgressColor,
|
||
fillProgressColor: lightFillProgressColor,
|
||
toString: () => name
|
||
},
|
||
DARK: {
|
||
name: "dark",
|
||
backgroundColor: darkBackgroundColor,
|
||
textColor: darkTextColor,
|
||
backgroundProgressColor: darkBackgroundProgressColor,
|
||
fillProgressColor: darkFillProgressColor,
|
||
toString: () => name
|
||
},
|
||
AUTO: {
|
||
name: "auto",
|
||
backgroundColor: autoBackgroundColor,
|
||
textColor: autoTextColor,
|
||
backgroundProgressColor: autoBackgroundProgressColor,
|
||
fillProgressColor: autoFillProgressColor,
|
||
toString: () => name
|
||
}
|
||
})
|
||
|
||
/**
|
||
* Enum for widget family.
|
||
* @readonly
|
||
* @enum {Symbol}
|
||
*/
|
||
const WidgetFamily = Object.freeze({
|
||
SMALL: enumValue("small"),
|
||
MEDIUM: enumValue("medium"),
|
||
LARGE: enumValue("large")
|
||
})
|
||
|
||
|
||
//////////////////// - EDIT ME - ///////////////////////////
|
||
|
||
/// Display mode
|
||
///
|
||
/// - DisplayMode.LIGHT: Light mode
|
||
/// - DisplayMode.DARK: Dark mode
|
||
/// - DisplayMode.AUTO: Follow system settings
|
||
|
||
const displayMode = DisplayMode.LIGHT
|
||
|
||
/// Debug mode: on / off
|
||
const debug = false
|
||
|
||
/// Debug input, following widget format:
|
||
/// - "<order-number>;<email>"
|
||
/// - "<order-number>;<email>;<item-number>"
|
||
///
|
||
/// ie. const debugInput = "W111111111;tim@apple.com;5"
|
||
const debugInput = null
|
||
|
||
/// Debug widget size (LARGE, MEDIUM or SMALL)
|
||
const debugWidgetFamily = WidgetFamily.MEDIUM
|
||
|
||
////////////////////////////////////////////////////////////
|
||
|
||
|
||
const cacheMinutes = 60 * 2
|
||
const today = new Date()
|
||
let width;
|
||
let widgetFamily;
|
||
const h = 5
|
||
const backgroundColor = displayMode.backgroundColor
|
||
const textColor = displayMode.textColor
|
||
const backgroundProgressColor = displayMode.backgroundProgressColor
|
||
const fillProgressColor = displayMode.fillProgressColor
|
||
|
||
if (debug && debugWidgetFamily !== null) {
|
||
widgetFamily = debugWidgetFamily
|
||
} else {
|
||
switch (config.widgetFamily) {
|
||
case 'small':
|
||
widgetFamily = WidgetFamily.SMALL
|
||
width = 200
|
||
break
|
||
case 'medium':
|
||
widgetFamily = WidgetFamily.MEDIUM
|
||
width = 400
|
||
break
|
||
case 'large':
|
||
widgetFamily = WidgetFamily.LARGE
|
||
width = 400
|
||
break
|
||
}
|
||
}
|
||
|
||
switch (widgetFamily) {
|
||
case WidgetFamily.SMALL:
|
||
width = 200
|
||
break
|
||
case WidgetFamily.MEDIUM:
|
||
width = 400
|
||
break
|
||
case WidgetFamily.LARGE:
|
||
width = 400
|
||
break
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////
|
||
let widgetInputRAW = args.widgetParameter;
|
||
let widgetInput;
|
||
if (widgetInputRAW !== null || (debug && debugInput !== null)) {
|
||
|
||
if (widgetInputRAW !== null) {
|
||
widgetInput = widgetInputRAW.toString().trim().split(';').map(v => v.trim())
|
||
} else {
|
||
widgetInput = debugInput.trim().split(';').map(v => v.trim())
|
||
}
|
||
|
||
if (!/^[A-Za-z][0-9]+/.test(widgetInput[0])) {
|
||
throw new Error('Invalid ordernumber format: "' + widgetInput[0] + '"')
|
||
}
|
||
if (widgetInput[2] && !/^[\d]+$/.test(widgetInput[2])) {
|
||
throw new Error('Third parameter has to be a number')
|
||
}
|
||
} else {
|
||
throw new Error('No Ordernumber and E-Mail address set')
|
||
}
|
||
////////////////////////////////////////////////////////////
|
||
const files = FileManager.local()
|
||
|
||
const path = files.joinPath(files.cacheDirectory(), "widget-apple-store-order-" + widgetInput[0])
|
||
|
||
const cacheExists = files.fileExists(path)
|
||
|
||
const cacheDate = cacheExists ? files.modificationDate(path) : 0
|
||
////////////////////////////////////////////////////////////
|
||
const localeText = {
|
||
default: ['Day', 'Days', {
|
||
'PLACED': 'Order Placed',
|
||
'PROCESSING': 'Processing',
|
||
'PREPARED_FOR_SHIPMENT': 'Preparing for Ship',
|
||
'SHIPPED': 'Shipped',
|
||
'DELIVERED': 'Delivered'
|
||
}],
|
||
en: ['Day', 'Days', {
|
||
'PLACED': 'Order Placed',
|
||
'PROCESSING': 'Processing',
|
||
'PREPARED_FOR_SHIPMENT': 'Preparing for Ship',
|
||
'SHIPPED': 'Shipped',
|
||
'DELIVERED': 'Delivered'
|
||
}],
|
||
de: ['Tag', 'Tage', {
|
||
'PLACED': 'Bestellung aufgegeben',
|
||
'PROCESSING': 'Vorgang läuft',
|
||
'PREPARED_FOR_SHIPMENT': 'Versand wird vorbereitet',
|
||
'SHIPPED': 'Bestellung versandt',
|
||
'DELIVERED': 'Geliefert'
|
||
}],
|
||
fr: ['Jour', 'Jours', {
|
||
'PLACED': 'Commande enregistrée',
|
||
'PROCESSING': 'Traitement',
|
||
'PREPARED_FOR_SHIPMENT': 'En cours de préparation pour expédition',
|
||
'SHIPPED': 'Expédiée',
|
||
'DELIVERED': 'Livrée'
|
||
}],
|
||
es: ['día', 'días', {
|
||
'PLACED': 'Pedido recibido',
|
||
'PROCESSING': 'Procesando',
|
||
'PREPARED_FOR_SHIPMENT': 'Preparando envío',
|
||
'SHIPPED': 'Enviado',
|
||
'DELIVERED': 'Entregado'
|
||
}],
|
||
it: ['giorno', 'giorni', {
|
||
'PLACED': 'Ordine inoltrato',
|
||
'PROCESSING': 'ElaborazioneIn',
|
||
'PREPARED_FOR_SHIPMENT': 'Spedizione in preparazione',
|
||
'SHIPPED': 'Spedito',
|
||
'DELIVERED': 'ConsegnatoIncompleto'
|
||
}]
|
||
}
|
||
////////////////////////////////////////////////////////////
|
||
const parseLongDate = (stringDate) => {
|
||
const months = {
|
||
'January': 0,
|
||
'February': 1,
|
||
'March': 2,
|
||
'April': 3,
|
||
'May': 4,
|
||
'June': 5,
|
||
'July': 6,
|
||
'August': 7,
|
||
'September': 8,
|
||
'October': 9,
|
||
'November': 10,
|
||
'December': 11
|
||
}
|
||
const m = stringDate.match(/([\w]+)[\s]([\d]{1,2}),[\s]([0-9]{4})/)
|
||
return new Date(m[3], months[m[1]], m[2])
|
||
}
|
||
const parseShortDate = (stringDate, orderDate) => {
|
||
const months = {
|
||
'Jan': 0,
|
||
'Feb': 1,
|
||
'Mar': 2,
|
||
'Apr': 3,
|
||
'May': 4,
|
||
'Jun': 5,
|
||
'Jul': 6,
|
||
'Aug': 7,
|
||
'Sep': 8,
|
||
'Oct': 9,
|
||
'Okt': 9,
|
||
'Nov': 10,
|
||
'Dec': 11,
|
||
'Dez': 11
|
||
}
|
||
let m
|
||
m = stringDate.match(/([\d]{1,2}) ([\w]{3})/)
|
||
if (!m) {
|
||
m = stringDate.match(/([\w]+),? ([\d]{1,2})/)
|
||
if (m) {
|
||
const t = m[1].slice(0, 3)
|
||
m[1] = m[2]
|
||
m[2] = t
|
||
} else {
|
||
throw new Error('Failed to extract the delivery date from string: ' + stringDate)
|
||
}
|
||
}
|
||
|
||
let deliveryDate = new Date((new Date().getFullYear()), months[m[2]], m[1])
|
||
if (deliveryDate < orderDate) {
|
||
deliveryDate.setFullYear(deliveryDate.getFullYear() + 1)
|
||
}
|
||
return deliveryDate
|
||
}
|
||
////////////////////////////////////////////////////////////
|
||
function creatProgress(total, havegone) {
|
||
const context = new DrawContext()
|
||
context.size = new Size(width, h)
|
||
context.opaque = false
|
||
context.respectScreenScale = true
|
||
context.setFillColor(backgroundProgressColor)
|
||
const path = new Path()
|
||
path.addRoundedRect(new Rect(0, 0, width, h), 3, 2)
|
||
context.addPath(path)
|
||
context.fillPath()
|
||
context.setFillColor(fillProgressColor)
|
||
const path1 = new Path()
|
||
const path1width = (width * havegone / total > width) ? width : width * havegone / total
|
||
path1.addRoundedRect(new Rect(0, 0, path1width, h), 3, 2)
|
||
context.addPath(path1)
|
||
context.fillPath()
|
||
return context.getImage()
|
||
}
|
||
////////////////////////////////////////////////////////////
|
||
const getTimeRemaining = function (endtime) {
|
||
const total = Date.parse(endtime) - Date.parse(new Date());
|
||
const seconds = Math.floor((total / 1000) % 60);
|
||
const minutes = Math.floor((total / 1000 / 60) % 60);
|
||
const hours = Math.floor((total / (1000 * 60 * 60)) % 24);
|
||
const days = Math.floor(total / (1000 * 60 * 60 * 24));
|
||
|
||
return {
|
||
total,
|
||
days,
|
||
hours,
|
||
minutes,
|
||
seconds
|
||
};
|
||
}
|
||
////////////////////////////////////////////////////////////
|
||
const getOrderdetails = async (ordernumber, email) => {
|
||
const reqSession = new Request('https://secure.store.apple.com/shop/order/list')
|
||
resSession = await reqSession.loadString()
|
||
|
||
const CookieValues = reqSession.response.cookies.map((v) => {
|
||
return v.name + "=" + v.value
|
||
})
|
||
|
||
const xAosStkMatch = resSession.match(/"x-aos-stk":"([\w-_]+)"/)
|
||
if (!xAosStkMatch) {
|
||
throw new Error('Needed x-aos-stk token not found')
|
||
}
|
||
const postUrl = (reqSession.response.url.replace('/orders', '/orderx')) + '&_a=guestUserOrderLookUp&_m=signIn.orderLookUp'
|
||
|
||
const postReq = new Request(postUrl)
|
||
postReq.headers = {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'Referer': reqSession.response.url,
|
||
'x-aos-model-page': 'olssSignInPage',
|
||
'x-aos-stk': xAosStkMatch[1],
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
'Cookie': CookieValues.join('; ')
|
||
}
|
||
postReq.method = "POST";
|
||
postReq.body = `signIn.orderLookUp.orderNumber=${ordernumber}&signIn.orderLookUp.emailAddress=${email}`
|
||
|
||
const resPostReq = await postReq.loadString()
|
||
|
||
if (postReq.response.statusCode !== 200) {
|
||
throw new Error(`Got HTTP ${postReq.response.statusCode} from API.`)
|
||
}
|
||
|
||
let postResData
|
||
try {
|
||
postResData = JSON.parse(resPostReq)
|
||
} catch (e) {
|
||
throw new Error('Can\'t parse API response.')
|
||
}
|
||
|
||
if (postResData['head']['status'] !== 302) {
|
||
throw new Error('Fetching the data failed. Got unexpected response. Please try it later.')
|
||
}
|
||
|
||
const req = new Request(postResData['head']['data']['url'])
|
||
const res = await req.loadString()
|
||
const rawJSON = res.match(/<script id="init_data" type="application\/json">[\s]+(.*)[\s]+<\/script>/)
|
||
if (!rawJSON) {
|
||
return null
|
||
}
|
||
const data = JSON.parse(rawJSON[1])
|
||
if (!data['orderDetail']) {
|
||
console.log(data)
|
||
throw new Error('no orderDetail attribute')
|
||
}
|
||
data.widgetURL = postResData['head']['data']['url']
|
||
return data
|
||
}
|
||
////////////////////////////////////////////////////////////
|
||
let orderDetails
|
||
if (cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheMinutes * 60 * 1000)) {
|
||
console.log("Get from Cache")
|
||
orderDetails = JSON.parse(files.readString(path))
|
||
} else {
|
||
console.log("Get from Website")
|
||
try {
|
||
orderDetails = await getOrderdetails(widgetInput[0], widgetInput[1])
|
||
if (orderDetails !== null) {
|
||
console.log("Write to Cache")
|
||
files.writeString(path, JSON.stringify(orderDetails))
|
||
}
|
||
} catch (e) {
|
||
console.error('Fetching data from website failed:')
|
||
console.error(e)
|
||
if (cacheExists) {
|
||
console.warn('Fallback to Cache')
|
||
orderDetails = JSON.parse(files.readString(path))
|
||
} else {
|
||
throw new Error('Fetching the data failed. No data to show.')
|
||
}
|
||
}
|
||
}
|
||
if (debug) {
|
||
console.log(JSON.stringify(orderDetails, null, 2))
|
||
}
|
||
|
||
let widget = new ListWidget();
|
||
|
||
if (!orderDetails) {
|
||
widget.addText('No order found')
|
||
} else {
|
||
// filter on orderItem to remove giveBackOrderItem
|
||
orderDetails['orderDetail']['orderItems']['c'] = orderDetails['orderDetail']['orderItems']['c'].filter((e) => {
|
||
return /orderItem-[\d]+/.test(e)
|
||
})
|
||
|
||
if (widgetInput[2] && !orderDetails['orderDetail']['orderItems']['c'][widgetInput[2] - 1]) {
|
||
throw new Error(`No Item on position ${widgetInput[2]}`)
|
||
}
|
||
const languageCode = Device.preferredLanguages()[0].match(/^[\a-z]{2}/)
|
||
|
||
const itemPosition = orderDetails['orderDetail']['orderItems']['c'][(widgetInput[2] - 1) || 0]
|
||
const itemDetails = orderDetails['orderDetail']['orderItems'][itemPosition]['orderItemDetails']
|
||
const itemStatusTracker = orderDetails['orderDetail']['orderItems'][itemPosition]['orderItemStatusTracker']
|
||
const orderDate = parseLongDate(orderDetails['orderDetail']['orderHeader']['d']['orderPlacedDate'])
|
||
let deliveryDate = null
|
||
try {
|
||
deliveryDate = parseShortDate(itemDetails['d']['deliveryDate'], orderDate)
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
|
||
const itemName = itemDetails['d']['productName']
|
||
const itemImageUrl = itemDetails['d']['imageData']['src'].replace(/wid=[\d]+/, 'wid=200').replace(/hei=[\d]+/, 'hei=200')
|
||
const itemImage = await(new Request(itemImageUrl)).loadImage()
|
||
const remainingDays = getTimeRemaining(deliveryDate).days + 1;
|
||
|
||
widget.setPadding(10, 10, 10, 10)
|
||
widget.backgroundColor = backgroundColor
|
||
widget.url = orderDetails.widgetURL
|
||
|
||
const headlineText = widget.addText(' Order Status')
|
||
headlineText.font = Font.regularSystemFont(14)
|
||
headlineText.textColor = textColor
|
||
headlineText.centerAlignText()
|
||
|
||
widget.addSpacer(5)
|
||
|
||
|
||
const productStack = widget.addStack()
|
||
productStack.layoutHorizontally()
|
||
|
||
const imageStack = productStack.addStack()
|
||
|
||
|
||
imageStack.backgroundColor = Color.white()
|
||
imageStack.size = new Size(37, 37)
|
||
imageStack.setPadding(1, 1, 1, 1)
|
||
imageStack.cornerRadius = 2
|
||
|
||
itemImageElement = imageStack.addImage(itemImage)
|
||
itemImageElement.imageSize = new Size(35, 35)
|
||
|
||
|
||
productStack.addSpacer(20)
|
||
|
||
rightProductStack = productStack.addStack()
|
||
rightProductStack.layoutVertically()
|
||
rightProductStack.addSpacer(5)
|
||
|
||
const itemNameText = rightProductStack.addText(itemName)
|
||
itemNameText.font = Font.regularSystemFont(10)
|
||
itemNameText.textColor = textColor
|
||
itemNameText.minimumScaleFactor = 0.5
|
||
|
||
if (widgetFamily === WidgetFamily.SMALL) {
|
||
itemNameText.lineLimit = 4
|
||
} else {
|
||
itemNameText.lineLimit = 2
|
||
}
|
||
|
||
widget.addSpacer()
|
||
if (deliveryDate !== null && itemStatusTracker['d']['currentStatus'] !== 'DELIVERED') {
|
||
const t = (localeText[languageCode]) ? localeText[languageCode] : localeText.default
|
||
let postFix = (remainingDays === 1) ? t[0] : t[1]
|
||
|
||
const remainingDayText = widget.addText(remainingDays + ' ' + postFix)
|
||
remainingDayText.font = Font.regularSystemFont(26)
|
||
remainingDayText.textColor = textColor
|
||
remainingDayText.centerAlignText()
|
||
remainingDayText.minimumScaleFactor = 0.5
|
||
|
||
widget.addSpacer()
|
||
|
||
const total = (deliveryDate - orderDate) / (1000 * 60 * 60 * 24)
|
||
const daysGone = total - remainingDays
|
||
|
||
const progressStack = widget.addStack()
|
||
progressStack.layoutVertically()
|
||
progressStack.spacing = 3
|
||
|
||
if (itemStatusTracker['d']['currentStatus']) {
|
||
let statusText
|
||
try {
|
||
const localeStatusText = (localeText[languageCode] && localeText[languageCode][2]) ? localeText[languageCode][2] : localeText.default[2]
|
||
statusText = progressStack.addText(localeStatusText[itemStatusTracker['d']['currentStatus']])
|
||
} catch (e) {
|
||
console.error(e)
|
||
statusText = progressStack.addText(itemStatusTracker['d']['currentStatus'])
|
||
}
|
||
statusText.textColor = textColor
|
||
statusText.font = Font.regularSystemFont(8)
|
||
}
|
||
|
||
progressStack.addImage(creatProgress(total, daysGone))
|
||
|
||
const footerStack = progressStack.addStack()
|
||
footerStack.layoutHorizontally()
|
||
|
||
const orderDateText = footerStack.addText(orderDate.toLocaleDateString())
|
||
orderDateText.textColor = textColor
|
||
orderDateText.font = Font.regularSystemFont(8)
|
||
orderDateText.lineLimit = 1
|
||
|
||
footerStack.addSpacer()
|
||
|
||
const deliveryDateText = footerStack.addText(deliveryDate.toLocaleDateString())
|
||
deliveryDateText.textColor = textColor
|
||
deliveryDateText.font = Font.regularSystemFont(8)
|
||
deliveryDateText.lineLimit = 1
|
||
} else {
|
||
widget.addSpacer()
|
||
|
||
fallbackStack = widget.addStack()
|
||
fallbackStack.layoutHorizontally()
|
||
fallbackStack.addSpacer()
|
||
|
||
let icon
|
||
let text
|
||
if (itemStatusTracker['d']['currentStatus'] === 'DELIVERED') {
|
||
icon = SFSymbol.named('house')
|
||
|
||
const localeStatusText = (localeText[languageCode] && localeText[languageCode][2]) ? localeText[languageCode][2] : localeText.default[2]
|
||
text = localeStatusText['DELIVERED']
|
||
} else if (itemDetails['d']['deliveryDate'] === 'Out for Delivery') {
|
||
icon = SFSymbol.named('shippingbox')
|
||
text = itemDetails['d']['deliveryDate'] // ToDO: Add translation
|
||
} else {
|
||
text = itemDetails['d']['deliveryDate']
|
||
}
|
||
if (icon) {
|
||
const iconStack = fallbackStack.addStack()
|
||
iconStack.layoutVertically()
|
||
iconStack.addSpacer()
|
||
|
||
const iconElement = iconStack.addImage(icon.image)
|
||
iconElement.imageSize = new Size(15, 15)
|
||
|
||
iconStack.addSpacer()
|
||
fallbackStack.addSpacer(5)
|
||
}
|
||
|
||
const fallbackTextStack = fallbackStack.addStack()
|
||
fallbackTextStack.layoutVertically()
|
||
fallbackTextStack.centerAlignContent()
|
||
fallbackTextStack.addSpacer()
|
||
|
||
const fallbackText = fallbackTextStack.addText(text)
|
||
fallbackText.font = Font.regularSystemFont(14)
|
||
fallbackText.textColor = textColor
|
||
fallbackText.minimumScaleFactor = 0.5
|
||
fallbackText.lineLimit = 1
|
||
|
||
fallbackTextStack.addSpacer()
|
||
|
||
fallbackStack.addSpacer()
|
||
|
||
widget.addSpacer()
|
||
}
|
||
}
|
||
|
||
if (!config.runsInWidget) {
|
||
// Present in debug mode
|
||
switch (widgetFamily) {
|
||
case WidgetFamily.SMALL:
|
||
await widget.presentSmall()
|
||
break
|
||
case WidgetFamily.MEDIUM:
|
||
await widget.presentMedium()
|
||
break
|
||
case WidgetFamily.LARGE:
|
||
await widget.presentLarge()
|
||
break
|
||
}
|
||
} else {
|
||
// Tell the system to show the widget.
|
||
Script.setWidget(widget)
|
||
Script.complete()
|
||
}
|