推し活ウィジェット
設定難易度

イベントの日付を登録しておけば、今日からイベント当日までの日数を毎日自動でカウントダウン。

推しの名前やイベント名、会場、推しカラーに加えて、iPhoneの写真ライブラリからお気に入りの写真を設定できます。

イベントは複数登録できるので、ツアーや複数公演に参加する場合にも対応。ひとつのイベントが終了すると、次に予定されているイベントへ自動的に切り替わります。

ウィジェットの表示内容

登録したイベントの日付から残り日数を計算します。

サイズごとの表示

  • 小:次のイベントまでの残り日数をコンパクトに表示します。
  • 中:推しの名前・イベント名・日付・会場などを確認できます。
  • 大:写真と予定を広いレイアウトで表示します。

設定手順

上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。

  1. Scriptableを用意する

    App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。

  2. コードをコピーして保存する

    下のコード欄の「コピー」を押し、Scriptableへ移動します。「+」から新しいスクリプトを作り、コード全文を貼り付けて「推しに会えるまで、あと何日?」などの名前で保存してください。

    保存できたら、このページに戻って次の「ウィジェットの設定を作る」へ進みます。

    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: pink; icon-glyph: heart;
    // Version: 1.1.0
    
    // =====================================================
    // 推しに会えるまで - Scriptable Widget
    // 複数イベント対応 完成版
    //
    // パラメーター形式:
    //
    // 推しの名前,推しカラー,イベント一覧
    //
    // イベント一覧:
    // 日付|イベント名|会場;日付|イベント名|会場
    //
    // 例:
    // 〇〇ちゃん,#FF69B4,2026-12-20|LIVE TOUR 大阪|京セラドーム大阪;2027-01-10|LIVE TOUR 東京|東京ドーム
    //
    // ・イベントはいくつでも登録可能
    // ・今日以降でもっとも近いイベントを自動表示
    // ・当日もそのイベントを表示
    // ・終了すると次のイベントへ自動切替
    // ・大サイズは次回予定も表示
    //
    // 写真設定:
    // ウィジェットをタップ、またはScriptableアプリで直接実行
    // →「写真を設定・変更」
    // → ウィジェットと同じ推しの名前を入力
    // → 写真を選択
    // =====================================================
    
    
    // =====================================================
    // 基本設定
    // =====================================================
    
    const DEFAULT_NAME = "推し";
    const DEFAULT_COLOR = "#FF69B4";
    
    
    // =====================================================
    // カラー
    // =====================================================
    
    const COLOR = {
      white:      new Color("#FFFFFF"),
      subWhite:   new Color("#FFFFFF", 0.78),
      background: new Color("#202020"),
      panel:      new Color("#000000", 0.50),
      panelLight: new Color("#000000", 0.35),
      line:       new Color("#FFFFFF", 0.20),
      warning:    new Color("#FFD166")
    };
    
    
    // =====================================================
    // FileManager
    // =====================================================
    
    const fm = FileManager.iCloud();
    
    const photoDir = fm.joinPath(
      fm.documentsDirectory(),
      "oshi_countdown_widget"
    );
    
    if (!fm.fileExists(photoDir)) {
      fm.createDirectory(photoDir);
    }
    
    
    // =====================================================
    // 共通関数
    // =====================================================
    
    function slugify(name) {
    
      return name
        .trim()
        .replace(
          /[^a-zA-Z0-9ぁ-んァ-ン一-龥]/g,
          "_"
        );
    }
    
    
    function getPhotoPath(name) {
    
      return fm.joinPath(
        photoDir,
        `photo_${slugify(name)}.jpg`
      );
    }
    
    
    function parseDate(str) {
    
      if (!str) {
        return null;
      }
    
      const d =
        new Date(
          str.trim() + "T00:00:00"
        );
    
      if (isNaN(d.getTime())) {
        return null;
      }
    
      return d;
    }
    
    
    function normalizeDate(date) {
    
      return new Date(
        date.getFullYear(),
        date.getMonth(),
        date.getDate()
      );
    }
    
    
    function daysBetween(from, to) {
    
      return Math.round(
        (
          normalizeDate(to) -
          normalizeDate(from)
        ) / 86400000
      );
    }
    
    
    // =====================================================
    // アプリ内で直接実行した場合
    // =====================================================
    
    // タップしたウィジェットの設定を、アプリ側に引き継ぎます。
    const query = typeof args !== "undefined" ? (args.queryParameters || {}) : {};
    const fromWidgetTap = query.action === "photoSettings";
    const rawParam = String(
      fromWidgetTap
        ? (query.widgetParam || "")
        : (typeof args !== "undefined" ? (args.widgetParameter || "") : "")
    ).trim();
    const photoName = rawParam ? parseMainParameter(rawParam).name : "";
    
    let choice = -1;
    
    if (!config.runsInWidget) {
    
      const menu =
        new Alert();
    
      menu.title =
        fromWidgetTap ? "写真の設定" : "推しに会えるまで";
    
      menu.message =
        fromWidgetTap
          ? `「${photoName || DEFAULT_NAME}」の写真を設定・変更、削除できます。`
          : "写真の設定やウィジェットのプレビューができます。";
    
      menu.addAction(
        "写真を設定・変更"
      );
    
      menu.addDestructiveAction(
        "写真を削除"
      );
    
      if (!fromWidgetTap) {
      menu.addAction(
        "小サイズをプレビュー"
      );
    
      menu.addAction(
        "中サイズをプレビュー"
      );
    
      menu.addAction(
        "大サイズをプレビュー"
      );
    
      }
    
      menu.addCancelAction(
        "終了"
      );
    
    
      choice =
        await menu.presentAlert();
    
    
      // ---------------------------------------------------
      // 写真設定
      // ---------------------------------------------------
    
      if (choice === 0) {
    
        const input =
          new Alert();
    
        input.title =
          "推しの名前";
    
        input.message =
          "ウィジェットのパラメーターと同じ推しの名前を入力してください。";
    
        input.addTextField(
          "例:〇〇ちゃん",
          photoName
        );
    
        input.addAction(
          "写真を選ぶ"
        );
    
        input.addCancelAction(
          "キャンセル"
        );
    
    
        const result =
          await input.presentAlert();
    
    
        if (result === -1) {
          Script.complete();
          return;
        }
    
    
        const name =
          input
            .textFieldValue(0)
            .trim();
    
    
        if (!name) {
          Script.complete();
          return;
        }
    
    
        try {
    
          const image =
            await Photos.fromLibrary();
    
          const path =
            getPhotoPath(name);
    
    
          fm.writeImage(
            path,
            image
          );
    
    
          const done =
            new Alert();
    
          done.title =
            "写真を保存しました";
    
          done.message =
            `「${name}」の写真を設定しました。`;
    
          done.addAction("OK");
    
          await done.presentAlert();
    
        } catch (e) {
    
          console.log(
            "写真設定エラー:" + e
          );
        }
    
    
        Script.complete();
        return;
      }
    
    
      // ---------------------------------------------------
      // 写真削除
      // ---------------------------------------------------
    
      if (choice === 1) {
    
        const input =
          new Alert();
    
        input.title =
          "写真を削除";
    
        input.message =
          "推しの名前を入力してください。";
    
        input.addTextField(
          "例:〇〇ちゃん",
          photoName
        );
    
        input.addDestructiveAction(
          "削除"
        );
    
        input.addCancelAction(
          "キャンセル"
        );
    
    
        const result =
          await input.presentAlert();
    
    
        if (result === -1) {
          Script.complete();
          return;
        }
    
    
        const name =
          input
            .textFieldValue(0)
            .trim();
    
    
        if (!name) {
          Script.complete();
          return;
        }
    
    
        const path =
          getPhotoPath(name);
    
        const alert =
          new Alert();
    
    
        if (fm.fileExists(path)) {
    
          fm.remove(path);
    
          alert.title =
            "削除しました";
    
          alert.message =
            `「${name}」の写真を削除しました。`;
    
        } else {
    
          alert.title =
            "写真がありません";
    
          alert.message =
            `「${name}」の写真は保存されていません。`;
        }
    
    
        alert.addAction("OK");
    
        await alert.presentAlert();
    
    
        Script.complete();
        return;
      }
    
    
      if (choice === -1) {
        Script.complete();
        return;
      }
    }
    
    
    // =====================================================
    // パラメーター
    // =====================================================
    
    const hasParam =
      rawParam.length > 0;
    
    
    // -----------------------------------------------------
    // 最初の2つの「,」だけを区切りとして使用
    // イベント名・会場内の読点などへの影響を減らす
    // -----------------------------------------------------
    
    function parseMainParameter(raw) {
    
      const firstComma =
        raw.indexOf(",");
    
      if (firstComma === -1) {
    
        return {
          name: raw.trim() || DEFAULT_NAME,
          color: DEFAULT_COLOR,
          eventString: ""
        };
      }
    
    
      const secondComma =
        raw.indexOf(
          ",",
          firstComma + 1
        );
    
    
      if (secondComma === -1) {
    
        return {
          name:
            raw
              .slice(0, firstComma)
              .trim() ||
            DEFAULT_NAME,
    
          color:
            raw
              .slice(firstComma + 1)
              .trim() ||
            DEFAULT_COLOR,
    
          eventString: ""
        };
      }
    
    
      return {
    
        name:
          raw
            .slice(0, firstComma)
            .trim() ||
          DEFAULT_NAME,
    
        color:
          raw
            .slice(
              firstComma + 1,
              secondComma
            )
            .trim() ||
          DEFAULT_COLOR,
    
        eventString:
          raw
            .slice(secondComma + 1)
            .trim()
      };
    }
    
    
    const parsed =
      parseMainParameter(rawParam);
    
    
    const oshiName =
      parsed.name;
    
    
    const accentHex =
      parsed.color;
    
    
    let accentColor;
    
    try {
    
      accentColor =
        new Color(accentHex);
    
    } catch (e) {
    
      accentColor =
        new Color(DEFAULT_COLOR);
    }
    
    
    // =====================================================
    // イベント一覧を解析
    // =====================================================
    
    function parseEvents(str) {
    
      if (!str) {
        return [];
      }
    
    
      return str
        .split(";")
        .map(item => {
    
          const parts =
            item
              .split("|")
              .map(s => s.trim());
    
    
          const dateString =
            parts[0] || "";
    
    
          const date =
            parseDate(dateString);
    
    
          if (!date) {
            return null;
          }
    
    
          return {
    
            dateString: dateString,
    
            date: date,
    
            name:
              parts[1] || "",
    
            venue:
              parts[2] || ""
          };
    
        })
        .filter(Boolean)
        .sort(
          (a, b) =>
            a.date - b.date
        );
    }
    
    
    const events =
      parseEvents(
        parsed.eventString
      );
    
    
    // =====================================================
    // 現在の日付
    // =====================================================
    
    const today =
      normalizeDate(
        new Date()
      );
    
    
    // =====================================================
    // 現在表示するイベントを決定
    //
    // 今日以降のイベントのうち
    // 一番近いものを表示
    // =====================================================
    
    const upcomingEvents =
      events.filter(
        event =>
          normalizeDate(event.date) >=
          today
      );
    
    
    const currentEvent =
      upcomingEvents.length > 0
        ? upcomingEvents[0]
        : null;
    
    
    const nextEvent =
      upcomingEvents.length > 1
        ? upcomingEvents[1]
        : null;
    
    
    // =====================================================
    // 全イベント終了後
    // =====================================================
    
    const lastEvent =
      events.length > 0
        ? events[events.length - 1]
        : null;
    
    
    const allEventsFinished =
      events.length > 0 &&
      !currentEvent;
    
    
    // =====================================================
    // 日数計算
    // =====================================================
    
    let diffDays = null;
    
    if (currentEvent) {
    
      diffDays =
        daysBetween(
          today,
          currentEvent.date
        );
    
    } else if (lastEvent) {
    
      diffDays =
        -daysBetween(
          lastEvent.date,
          today
        );
    }
    
    
    // =====================================================
    // カウントダウン表示
    // =====================================================
    
    function getCountdown() {
    
      if (events.length === 0) {
    
        return {
          type: "none",
          main: "--",
          unit: "",
          label: "イベントを設定してください"
        };
      }
    
    
      if (allEventsFinished) {
    
        const passed =
          Math.abs(diffDays);
    
        if (passed === 1) {
    
          return {
            type: "after",
            main: "1",
            unit: "DAY",
            label: "推しに会ってから"
          };
        }
    
    
        return {
          type: "after",
          main: `${passed}`,
          unit: "DAYS",
          label: "推しに会ってから"
        };
      }
    
    
      if (diffDays > 1) {
    
        return {
          type: "before",
          main: `${diffDays}`,
          unit: "DAYS",
          label: "会えるまであと"
        };
      }
    
    
      if (diffDays === 1) {
    
        return {
          type: "tomorrow",
          main: "明日!",
          unit: "",
          label: "いよいよ"
        };
      }
    
    
      return {
        type: "today",
        main: "TODAY",
        unit: "",
        label: "今日、推しに会える ♡"
      };
    }
    
    
    const countdown =
      getCountdown();
    
    
    // =====================================================
    // 表示対象イベント
    // =====================================================
    
    const displayEvent =
      currentEvent ||
      lastEvent;
    
    
    // =====================================================
    // 日付表示
    // =====================================================
    
    function formatDate(date) {
    
      if (!date) {
        return "";
      }
    
      const formatter =
        new DateFormatter();
    
      formatter.dateFormat =
        "yyyy.MM.dd";
    
      return formatter.string(date);
    }
    
    
    function formatShortDate(date) {
    
      if (!date) {
        return "";
      }
    
      const formatter =
        new DateFormatter();
    
      formatter.dateFormat =
        "M.d";
    
      return formatter.string(date);
    }
    
    
    function formatWeekday(date) {
    
      if (!date) {
        return "";
      }
    
      const formatter =
        new DateFormatter();
    
      formatter.dateFormat =
        "EEE";
    
      return formatter
        .string(date)
        .toUpperCase();
    }
    
    
    const dateDisplay =
      displayEvent
        ? formatDate(displayEvent.date)
        : "";
    
    
    const weekday =
      displayEvent
        ? formatWeekday(displayEvent.date)
        : "";
    
    
    // =====================================================
    // 写真読み込み
    // =====================================================
    
    async function loadPhoto(name) {
    
      const path =
        getPhotoPath(name);
    
    
      if (!fm.fileExists(path)) {
        return null;
      }
    
    
      try {
    
        await fm.downloadFileFromiCloud(
          path
        );
    
        return fm.readImage(path);
    
      } catch (e) {
    
        console.log(
          "写真読み込みエラー:" + e
        );
    
        return null;
      }
    }
    
    
    const photoImage =
      await loadPhoto(oshiName);
    
    
    const hasPhoto =
      photoImage !== null;
    
    
    // =====================================================
    // ウィジェット
    // =====================================================
    
    const widget =
      new ListWidget();
    
    // Scriptableを開き、このスクリプトの写真設定メニューを表示します。
    const runURL = URLScheme.forRunningScript();
    widget.url = runURL + (runURL.includes("?") ? "&" : "?")
      + "action=photoSettings&widgetParam=" + encodeURIComponent(rawParam);
    
    
    
    // =====================================================
    // サイズ判定
    // =====================================================
    
    let family =
      config.widgetFamily ||
      "medium";
    
    
    if (!config.runsInWidget) {
    
      if (choice === 2) {
        family = "small";
      }
    
      if (choice === 3) {
        family = "medium";
      }
    
      if (choice === 4) {
        family = "large";
      }
    }
    
    
    // =====================================================
    // 背景
    // =====================================================
    
    if (hasPhoto) {
    
      widget.backgroundImage =
        photoImage;
    
    } else {
    
      widget.backgroundColor =
        COLOR.background;
    }
    
    
    // =====================================================
    // 共通パーツ
    // =====================================================
    
    function addAccentLine(
      stack,
      width = 28
    ) {
    
      const line =
        stack.addStack();
    
      line.size =
        new Size(
          width,
          3
        );
    
      line.backgroundColor =
        accentColor;
    
      line.cornerRadius = 2;
    }
    
    
    // -----------------------------------------------------
    // イベント情報
    // -----------------------------------------------------
    
    function addEventInfo(
      stack,
      event,
      eventSize,
      venueSize
    ) {
    
      if (!event) {
        return;
      }
    
    
      if (event.name) {
    
        const title =
          stack.addText(
            event.name
          );
    
        title.font =
          Font.boldSystemFont(
            eventSize
          );
    
        title.textColor =
          COLOR.white;
    
        title.lineLimit = 1;
    
        title.minimumScaleFactor =
          0.6;
    
    
        if (event.venue) {
          stack.addSpacer(2);
        }
      }
    
    
      if (event.venue) {
    
        const venue =
          stack.addText(
            event.venue
          );
    
        venue.font =
          Font.systemFont(
            venueSize
          );
    
        venue.textColor =
          COLOR.subWhite;
    
        venue.lineLimit = 1;
    
        venue.minimumScaleFactor =
          0.6;
      }
    }
    
    
    // -----------------------------------------------------
    // カウントダウン
    // -----------------------------------------------------
    
    function addCountdown(
      stack,
      numberSize,
      specialSize,
      labelSize,
      unitSize
    ) {
    
      const label =
        stack.addText(
          countdown.label
        );
    
      label.font =
        Font.boldSystemFont(
          labelSize
        );
    
      label.textColor =
        COLOR.white;
    
      label.lineLimit = 1;
    
      label.minimumScaleFactor =
        0.65;
    
    
      stack.addSpacer(1);
    
    
      const row =
        stack.addStack();
    
      row.layoutHorizontally();
    
      row.bottomAlignContent();
    
    
      const main =
        row.addText(
          countdown.main
        );
    
      main.font =
        Font.boldSystemFont(
          countdown.type === "before" ||
          countdown.type === "after"
            ? numberSize
            : specialSize
        );
    
      main.textColor =
        COLOR.white;
    
      main.lineLimit = 1;
    
      main.minimumScaleFactor =
        0.45;
    
    
      if (countdown.unit) {
    
        row.addSpacer(6);
    
    
        const unit =
          row.addText(
            countdown.unit
          );
    
        unit.font =
          Font.boldSystemFont(
            unitSize
          );
    
        unit.textColor =
          accentColor;
      }
    }
    
    
    // =====================================================
    // SMALL
    // =====================================================
    
    function buildSmall() {
    
      widget.setPadding(
        10,
        10,
        10,
        10
      );
    
    
      // ---------------------------------------------------
      // 推し名
      // ---------------------------------------------------
    
      const namePanel =
        widget.addStack();
    
      namePanel.backgroundColor =
        COLOR.panelLight;
    
      namePanel.cornerRadius = 9;
    
      namePanel.setPadding(
        5,
        7,
        5,
        7
      );
    
    
      const name =
        namePanel.addText(
          oshiName
        );
    
      name.font =
        Font.boldSystemFont(12);
    
      name.textColor =
        COLOR.white;
    
      name.lineLimit = 1;
    
      name.minimumScaleFactor =
        0.65;
    
    
      widget.addSpacer();
    
    
      // ---------------------------------------------------
      // カウントダウン
      // ---------------------------------------------------
    
      const panel =
        widget.addStack();
    
      panel.layoutVertically();
    
      panel.backgroundColor =
        COLOR.panel;
    
      panel.cornerRadius = 12;
    
      panel.setPadding(
        8,
        9,
        8,
        9
      );
    
    
      addAccentLine(
        panel,
        22
      );
    
    
      panel.addSpacer(4);
    
    
      addCountdown(
        panel,
        34,
        23,
        9,
        9
      );
    
    
      if (displayEvent) {
    
        panel.addSpacer(3);
    
    
        const date =
          panel.addText(
            `${dateDisplay} ${weekday}`
          );
    
        date.font =
          Font.systemFont(8);
    
        date.textColor =
          COLOR.subWhite;
    
        date.lineLimit = 1;
      }
    }
    
    
    // =====================================================
    // MEDIUM
    // =====================================================
    
    function buildMedium() {
    
      widget.setPadding(
        12,
        13,
        12,
        13
      );
    
    
      // ---------------------------------------------------
      // ヘッダー
      // ---------------------------------------------------
    
      const header =
        widget.addStack();
    
      header.backgroundColor =
        COLOR.panelLight;
    
      header.cornerRadius = 10;
    
      header.setPadding(
        6,
        8,
        6,
        8
      );
    
      header.layoutHorizontally();
    
      header.centerAlignContent();
    
    
      const name =
        header.addText(
          oshiName
        );
    
      name.font =
        Font.boldSystemFont(13);
    
      name.textColor =
        COLOR.white;
    
      name.lineLimit = 1;
    
    
      header.addSpacer();
    
    
      if (displayEvent) {
    
        const date =
          header.addText(
            `${dateDisplay} ${weekday}`
          );
    
        date.font =
          Font.systemFont(9);
    
        date.textColor =
          COLOR.subWhite;
    
        date.lineLimit = 1;
      }
    
    
      widget.addSpacer();
    
    
      // ---------------------------------------------------
      // メインパネル
      // ---------------------------------------------------
    
      const panel =
        widget.addStack();
    
      panel.layoutHorizontally();
    
      panel.backgroundColor =
        COLOR.panel;
    
      panel.cornerRadius = 13;
    
      panel.setPadding(
        9,
        11,
        9,
        11
      );
    
    
      // ---------------------------------------------------
      // 左:カウントダウン
      // ---------------------------------------------------
    
      const countArea =
        panel.addStack();
    
      countArea.layoutVertically();
    
    
      addAccentLine(
        countArea,
        27
      );
    
    
      countArea.addSpacer(4);
    
    
      addCountdown(
        countArea,
        36,
        26,
        9,
        9
      );
    
    
      // ---------------------------------------------------
      // 右:イベント
      // ---------------------------------------------------
    
      if (
        displayEvent &&
        (
          displayEvent.name ||
          displayEvent.venue
        )
      ) {
    
        panel.addSpacer(16);
    
    
        const info =
          panel.addStack();
    
        info.layoutVertically();
    
        info.centerAlignContent();
    
    
        addEventInfo(
          info,
          displayEvent,
          10,
          8
        );
      }
    }
    
    
    // =====================================================
    // LARGE
    // =====================================================
    
    function buildLarge() {
    
      widget.setPadding(
        16,
        16,
        16,
        16
      );
    
    
      // ---------------------------------------------------
      // ヘッダー
      // ---------------------------------------------------
    
      const header =
        widget.addStack();
    
      header.backgroundColor =
        COLOR.panelLight;
    
      header.cornerRadius = 11;
    
      header.setPadding(
        7,
        10,
        7,
        10
      );
    
      header.layoutHorizontally();
    
      header.centerAlignContent();
    
    
      const name =
        header.addText(
          oshiName
        );
    
      name.font =
        Font.boldSystemFont(16);
    
      name.textColor =
        COLOR.white;
    
      name.lineLimit = 1;
    
    
      header.addSpacer();
    
    
      if (displayEvent) {
    
        const date =
          header.addText(
            `${dateDisplay} ${weekday}`
          );
    
        date.font =
          Font.systemFont(10);
    
        date.textColor =
          COLOR.subWhite;
      }
    
    
      widget.addSpacer();
    
    
      // ---------------------------------------------------
      // メインパネル
      // ---------------------------------------------------
    
      const panel =
        widget.addStack();
    
      panel.layoutVertically();
    
      panel.backgroundColor =
        COLOR.panel;
    
      panel.cornerRadius = 16;
    
      panel.setPadding(
        14,
        15,
        14,
        15
      );
    
    
      addAccentLine(
        panel,
        36
      );
    
    
      panel.addSpacer(7);
    
    
      addCountdown(
        panel,
        54,
        36,
        12,
        11
      );
    
    
      // ---------------------------------------------------
      // 現在のイベント
      // ---------------------------------------------------
    
      if (
        displayEvent &&
        (
          displayEvent.name ||
          displayEvent.venue
        )
      ) {
    
        panel.addSpacer(10);
    
    
        const divider =
          panel.addStack();
    
        divider.size =
          new Size(
            0,
            1
          );
    
        divider.backgroundColor =
          COLOR.line;
    
    
        panel.addSpacer(9);
    
    
        addEventInfo(
          panel,
          displayEvent,
          14,
          10
        );
      }
    
    
      // ---------------------------------------------------
      // 次の予定
      // ---------------------------------------------------
    
      if (nextEvent) {
    
        panel.addSpacer(12);
    
    
        const nextBox =
          panel.addStack();
    
        nextBox.layoutVertically();
    
        nextBox.backgroundColor =
          new Color(
            "#FFFFFF",
            0.12
          );
    
        nextBox.cornerRadius = 10;
    
        nextBox.setPadding(
          8,
          10,
          8,
          10
        );
    
    
        const nextLabel =
          nextBox.addText(
            "NEXT"
          );
    
        nextLabel.font =
          Font.boldSystemFont(8);
    
        nextLabel.textColor =
          accentColor;
    
    
        nextBox.addSpacer(3);
    
    
        const nextRow =
          nextBox.addStack();
    
        nextRow.layoutHorizontally();
    
        nextRow.centerAlignContent();
    
    
        const nextDate =
          nextRow.addText(
            formatShortDate(
              nextEvent.date
            )
          );
    
        nextDate.font =
          Font.boldSystemFont(12);
    
        nextDate.textColor =
          COLOR.white;
    
    
        if (nextEvent.name) {
    
          nextRow.addSpacer(8);
    
    
          const nextName =
            nextRow.addText(
              nextEvent.name
            );
    
          nextName.font =
            Font.systemFont(11);
    
          nextName.textColor =
            COLOR.white;
    
          nextName.lineLimit = 1;
    
          nextName.minimumScaleFactor =
            0.6;
        }
    
    
        if (nextEvent.venue) {
    
          nextBox.addSpacer(2);
    
    
          const nextVenue =
            nextBox.addText(
              nextEvent.venue
            );
    
          nextVenue.font =
            Font.systemFont(9);
    
          nextVenue.textColor =
            COLOR.subWhite;
    
          nextVenue.lineLimit = 1;
    
          nextVenue.minimumScaleFactor =
            0.6;
        }
      }
    }
    
    
    // =====================================================
    // サイズごとに作成
    // =====================================================
    
    if (family === "small") {
    
      buildSmall();
    
    } else if (family === "large") {
    
      buildLarge();
    
    } else {
    
      buildMedium();
    }
    
    
    // =====================================================
    // パラメーター未設定
    // =====================================================
    
    if (!hasParam) {
    
      const hint =
        widget.addText(
          "パラメーター未設定"
        );
    
      hint.font =
        Font.systemFont(7);
    
      hint.textColor =
        COLOR.warning;
    }
    
    
    // =====================================================
    // 表示
    // =====================================================
    
    if (config.runsInWidget) {
    
      Script.setWidget(widget);
    
    } else {
    
      if (family === "small") {
    
        await widget.presentSmall();
    
      } else if (family === "large") {
    
        await widget.presentLarge();
    
      } else {
    
        await widget.presentMedium();
      }
    }
    
    
    Script.complete();
  3. ウィジェットの設定を作る

    下の入力欄で自分用の設定を作り、生成されたパラメーターをコピーします。詳しい設定仕様は、設定手順の下でも確認できます。

    推し活パラメーター作成

    必要な項目を入力すると、ウィジェット用のパラメーターを自動で作成します。

    推しの設定



    会える予定

    生成されたパラメーター


  4. ホーム画面に追加して設定する

    iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。

    追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。

    「Parameter」に直前の手順でコピーした値を貼り付けます。設定を変えるときも、同じ入力欄で作り直して貼り替えてください。

    編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。

  5. 推しの写真を設定

    次に、ウィジェットに表示する写真を設定します。

    Scriptableを開き、先ほど作成した「推しに会えるまで」のスクリプトをタップして直接実行します。

    メニューが表示されたら、

    「写真を設定・変更」

    を選択してください。

    「推しの名前」を入力する画面が表示されます。

    ここには、パラメーター作成ツールで入力したものとまったく同じ推しの名前を入力してください。

    たとえばパラメーターで、

    〇〇ちゃん

    と設定した場合、写真設定でも、

    〇〇ちゃん

    と入力します。

    名前を入力して「写真を選ぶ」をタップすると、iPhoneの写真ライブラリが開きます。

    ウィジェットに使用したい写真を1枚選択してください。

    選択した写真がウィジェットの背景として保存されます。

    ※推しの名前が違うと写真を読み込めないため、同じ名前を入力してください。

    写真はあとから何度でも変更できます。

    写真を変更したい場合も、同じメニューから「写真を設定・変更」を選択してください。

    写真の設定方法。ウィジェットをタップするところから写真の登録・変更まで。緑の印がタップ位置です。写真選択画面の一覧は、プライバシー保護のためモザイク処理しています。

パラメータ・カスタマイズ

パラメーターの設定について

このウィジェットでは、

  • 推しの名前
  • 推しカラー
  • イベントの日付
  • イベント名
  • 会場

を「パラメーター」として設定します。

自分で複雑な文字列を入力する必要はありません。

下記の「パラメーター作成ツール」に必要な情報を入力すると、Scriptableに貼り付けるためのパラメーターが自動で作成されます。

推しの名前

ウィジェットに表示したい推しの名前を入力してください。

この名前は、あとで写真を設定するときにも使用します。

推しカラー

推しカラーを選択します。

カラーピッカーをタップして好きな色を選ぶだけで、自動的にカラーコードが設定されます。

ここで選んだ色は、ウィジェット内のアクセントカラーとして使用されます。

会える予定

イベントの日付をカレンダーから選択します。

イベント名と会場も入力できます。

例:

イベント名
「LIVE TOUR 2026 大阪」

会場
「京セラドーム大阪」

イベント名と会場は空欄でも使用できます。

複数のイベントを登録する場合

ライブツアーなどで複数の予定がある場合は、「+ イベントを追加」を押してください。

イベントの入力欄が追加されるので、同じように日付・イベント名・会場を入力します。

イベントはいくつでも追加できます。

ウィジェットには、今日からもっとも近いイベントが自動的に表示されます。

大阪公演が終了すると、翌日から東京公演へのカウントダウンへ自動的に切り替わります。

パラメーター 作成

下記の「推し活パラメーター作成」を使って必要な項目を入力すると、画面下に「生成されたパラメーター」が自動で表示されます。

「パラメーターをコピー」を押してください。

そのままiPhoneのウィジェット設定の「parameter」枠に貼り付けたら設定完了です。

イベント当日になると?

残り日数は毎日自動で更新されます。

通常は、

「会えるまであと 86 DAYS」

と表示。

前日になると、

「いよいよ 明日!」

当日は、

「今日、推しに会える ♡」

へ自動的に変わります。

複数のイベントを登録している場合は、イベント終了後に次の予定へ自動で切り替わります。

登録しているイベントがすべて終了すると、最後に会った日から、

「推しに会ってから ○ DAYS」

と表示されます。

一度予定を登録しておけば、あとはホーム画面を見るだけ。

推しに会える日が少しずつ近づいてくるのを、毎日楽しめるウィジェットです。

動作確認・使用結果

確認した端末 iPhone 17 ProMAX
iOS iOS27
Scriptable 1.7.19
確認したサイズ 小・中・大
バージョン 1.1.0
動作確認日 2026-10-13

表示・使用時のポイント

登録したイベントの日付から残り日数を計算します。写真は推しの名前にひも付けて保存し、イベント当日や終了後は日付に応じた表示へ切り替わります。

更新履歴

1.1.0

  • ウィジェットをタップすると写真の設定メニューが開くよう変更
  • タップしたウィジェットの推しの名前を写真設定・削除画面に自動入力

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
写真が表示されない
写真設定で入力した名前とParameterの推しの名前を完全に一致させてください。
別のイベントが出る
複数の予定を登録した場合、次に近い予定が優先されます。登録日付を確認してください。
日数が合わない
フォームの日付と端末の日付を確認してください。

注意事項・制限

  • 日付単位の計算です。開演時刻までの秒単位のカウントダウンではありません。
  • 写真は自分で設定する方式です。イベント情報や写真を外部から自動取得する機能はありません。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

このウィジェットにはデータ提供や参考資料は特にありません。

SHARE

この記事をシェア