zemlan.in

iOS Lock Screen, Powered by Microsoft

Где компромиссное решение в сто строк кода заменяется бескомпромиссным в пять прямоугольников со скруглёнными углами

Первым виджетом, который я набросал в Scriptable, был список приложений с виндовой фотографией дня (она же — "фотография на фоне bing.com")

Мне нравится "свежесть" экрана блокировки Windows и хотелось иметь что-то подобное и на телефоне. В бетах iOS 13 во встроенном приложении Shortcuts ("Команды") даже было действие для установки произвольной картинки изображением на домашний экран и/или экран блокировки. Но оно не дожило до релиза iOS 13 и не вернулось к релизу iOS 14

Компромисс

Так что единственной возможностью иметь что-то близкое к динамической обоине из интернета было сделать обновляющийся виджет. Ссылку на волшебный JSON со ссылкой на фотографию дня нашёл, само собой, на Stack Overflow. Список ссылок/приложений поверх картинки добавил чуть позже, как чтобы домашний экран не терял своё главное назначение — запуск этих приложений

Спустя ещё некоторое время, добавил кэширование фотографии дня, чтобы телефон не загружал её каждый раз, когда iOS решала обновить виджет. Я не сохранял промежуточные версии скрипта, но сейчас он выглядит как-то так (пропустить простынку кода и перейти к более интересной части):

const APPS = [
  { url: "photos-redirect://", name: "photos" },
  { url: "App-prefs://", name: "settings" },
  { separator: true },
  { url: "bear://" },
  { url: "things://" },
  { url: "reeder://" },
  { url: "twitterrific://" },
  { separator: true },
  { url: "spotify://" },
  { url: "overcast://" },
  // { url: "music://" },
  { separator: true },
  { url: "onepassword://", name: "1password" },
  { url: "otpauth://", name: "authenticator" },
  // { url: "diia.app://", name: "diia" },
];

const NOW = new Date;

const widget = new ListWidget();
widget.setPadding(0, 0, 0, 0);
widget.backgroundColor = Color.black();
// refresh every 4 hours
widget.refreshAfterDate = new Date(+NOW + 1000 * 60 * 60 * 4);

function formatDate(date, format) {
  const formatter = new DateFormatter()
  formatter.dateFormat = format
  return formatter.string(date)
}

async function getTodaysImage() {
  const fm = FileManager.local()
  const imagesDirectory = `${fm.cacheDirectory()}/${Script.name()}`
  
  if (!fm.fileExists(imagesDirectory)) {
    fm.createDirectory(imagesDirectory, true)
  }

  const startdate = formatDate(NOW, "YYYYMMdd")
  const metadataPath = `${imagesDirectory}/${startdate}.json`
  
  let todaysImage
  
  if (fm.fileExists(metadataPath)) {
    todaysImage = JSON.parse(fm.readString(metadataPath))
  } else {
    const bingReq = new Request(
      "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1"
    );
    const { images } = await bingReq.loadJSON();
    todaysImage = images[0];
    
    if (startdate === todaysImage.startdate) {
      fm.writeString(metadataPath, JSON.stringify(todaysImage))
    }
  }
  
  const imagePath = `${imagesDirectory}/${todaysImage.startdate}.jpg`
  
  if (fm.fileExists(imagePath)) {
    return {
      image: fm.readImage(imagePath),
      copyrightlink: todaysImage.copyrightlink
    }
  }
  
  const image = await new Request(
    "https://www.bing.com" + todaysImage.url
  ).loadImage()
  
  fm.writeImage(imagePath, image)

  return {
    image,
    copyrightlink: todaysImage.copyrightlink,
  };
}

const { image, copyrightlink } = await getTodaysImage();

widget.backgroundImage = image;

const row = widget.addStack();

row.layoutHorizontally();

const copyrightColumn = row.addStack();
copyrightColumn.layoutVertically();
copyrightColumn.addSpacer(null);
copyrightColumn.setPadding(16, 16, 16, 16);

const copyrightSymbol = SFSymbol.named("c.circle");
copyrightSymbol.applyThinWeight();
const c = copyrightColumn.addImage(copyrightSymbol.image);
c.url = copyrightlink;
c.resizable = false;
c.tintColor = Color.white();

row.addSpacer(null);

const column = row.addStack();
column.layoutVertically();
column.setPadding(14, 0, 12, 0);

for (const { separator, url, name } of APPS) {
  if (separator) {
    column.addSpacer(null);
    continue;
  }
  const appRow = column.addStack();
  appRow.addSpacer(null);
  appRow.setPadding(2, 16, 4, 16);

  const t = appRow.addText(name || url.replace(/:\/\/$/, ""));
  t.rightAlignText();
  t.textColor = Color.white();
  t.shadowColor = Color.black();
  t.shadowRadius = 4;
  t.font = new Font("Menlo", 18);

  appRow.url = url;
}

widget.presentLarge();

Получил, что хотел

К счастью, в iOS 14.3 в Shortcuts вернулось действие "Установить X в качестве обоев", так что "динамический экран блокировки" стал возможен без необходимости в компромиссах. Этот shortcut делает необходимый минимум и ничего больше:

  1. Постучаться по ссылке https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
  2. Получить из ответа адрес сегодняшней фотографии. Shortcuts считает по-человечески, начиная с единицы, так что "url первого элемента списка images" доступно по ключу images.1.url, без программистских "мы считаем с нуля потому что наши деды считали с нуля"
  3. Постучаться по ссылке из прошлого шага, дописав к ней https://bing.com/1. Для комбинирования фиксированного текста с результатами предыдущего действия есть специальное действие, но можно "срезать угол" и сделать это с помощью "волшебной переменной"
  4. Собственно, установить новую обоину. Чтобы телефон не просил никаких подтверждений и тупо использовал центр фотографии, я выключил опцию "Показать окно просмотра"

Чтобы автоматически запускать эту команду каждый день (например, когда заходит солнце), в Shortcuts есть вкладка "Автоматизация":

И готово! Теперь каждый вечер мой экран блокировки обновляется красивой фотографией ☺️

Ссылки

P.S. // 2021-05-02

Anton Verinov on Twitter
Anton Verinov on Twitter
Иногда, автоматический кроппинг порождает шедевры


  1. / в конце необязательный, но с ним скриншот читается немного получше ↩︎