diff --git a/_src/components/MainMirrorList.vue b/_src/components/MainMirrorList.vue
index 9b6dbf1..e0ed34a 100644
--- a/_src/components/MainMirrorList.vue
+++ b/_src/components/MainMirrorList.vue
@@ -1,48 +1,37 @@
diff --git a/_src/lib/mirrorList.js b/_src/lib/mirrorList.js
new file mode 100644
index 0000000..81185b1
--- /dev/null
+++ b/_src/lib/mirrorList.js
@@ -0,0 +1,125 @@
+import { TUNASYNC_JSON_PATH } from "../lib/consts";
+import { options as globalOptions } from "virtual:jekyll-data";
+import { ref, onMounted, nextTick } from "vue";
+import { format as TimeAgoFormat } from "timeago.js";
+
+const label_map = globalOptions.label_map;
+
+export const useMirrorList = (additional = []) => {
+ const mirrorList = ref([]);
+ let refreshTimer = null;
+
+ const refreshMirrorList = async () => {
+ if (document.hidden === true) {
+ return;
+ }
+ try {
+ const res = await fetch(TUNASYNC_JSON_PATH);
+ const status_data = await res.json();
+ const processed = status_data
+ .concat(additional)
+ .map((d) => processMirrorItem(d));
+ mirrorList.value = sortAndUniqMirrors(processLinkItem(processed));
+ } catch (e) {
+ throw e;
+ } finally {
+ refreshTimer = setTimeout(refreshMirrorList, 10000);
+ }
+ };
+
+ nextTick().then(() => refreshMirrorList());
+
+ onMounted(() => {
+ window.addEventListener("visibilitychange", () => {
+ if (refreshTimer) {
+ clearTimeout(refreshTimer);
+ refreshTimer = null;
+ }
+ if (document.visibilityState === "visible") {
+ refreshMirrorList().then();
+ }
+ });
+ });
+
+ return mirrorList;
+};
+
+const processLinkItem = (mirrors) => {
+ var processed = [];
+ for (let d of mirrors) {
+ if (d.link_to === undefined) {
+ processed.push(d);
+ continue;
+ }
+ for (const target of mirrors) {
+ if (d.link_to === target.name) {
+ d.status = target.status;
+ d.label = target.label;
+ d.upstream = target.upstream;
+ d.show_status = target.show_status;
+ d.last_update = target.last_update;
+ d.last_update_ago = target.last_update_ago;
+ d.last_ended = target.last_ended;
+ d.last_ended_ago = target.last_ended_ago;
+ d.last_schedule = target.last_schedule;
+ d.last_schedule_ago = target.last_schedule_ago;
+ processed.push(d);
+ break;
+ }
+ }
+ }
+ return processed;
+};
+
+const stringifyTime = (ts) => {
+ const date = new Date(ts * 1000);
+ let str = "";
+ let ago = "";
+ if (date.getFullYear() > 2000) {
+ str =
+ `${("000" + date.getFullYear()).slice(-4)}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}` +
+ ` ${("0" + date.getHours()).slice(-2)}:${("0" + date.getMinutes()).slice(-2)}`;
+ ago = TimeAgoFormat(date);
+ } else {
+ str = "0000-00-00 00:00";
+ ago = "Never";
+ }
+ return [str, ago];
+};
+
+const processMirrorItem = (d) => {
+ if (d.is_master === undefined) {
+ d.is_master = true;
+ }
+ if (d.link_to !== undefined) {
+ return d;
+ }
+ d.label = label_map[d.status];
+ d.show_status = d.status != "success";
+ // Strip the second component of last_update
+ [d.last_update, d.last_update_ago] = stringifyTime(d.last_update_ts);
+ [d.last_ended, d.last_ended_ago] = stringifyTime(d.last_ended_ts);
+ [d.last_started, d.last_started_ago] = stringifyTime(d.last_started_ts);
+ [d.next_schedule, d.next_schedule_ago] = stringifyTime(d.next_schedule_ts);
+ return d;
+};
+
+const sortAndUniqMirrors = (mirs) => {
+ mirs.sort((a, b) => {
+ return a.name < b.name ? -1 : 1;
+ });
+ return mirs.reduce((acc, cur) => {
+ if (acc.length > 1 && acc[acc.length - 1].name == cur.name) {
+ if (acc[acc.length - 1].last_update_ts && cur.last_update_ts) {
+ if (acc[acc.length - 1].last_update_ts < cur.last_update_ts) {
+ acc[acc.length - 1] = cur;
+ }
+ } else if (cur.last_update_ts) {
+ acc[acc.length - 1] = cur;
+ }
+ } else {
+ acc.push(cur);
+ }
+ return acc;
+ }, []);
+};