Skip to content

Instantly share code, notes, and snippets.

@Hiryuto-oecu
Created April 13, 2026 01:01
Show Gist options
  • Select an option

  • Save Hiryuto-oecu/d02a9233be68fb63328c04b0614b4290 to your computer and use it in GitHub Desktop.

Select an option

Save Hiryuto-oecu/d02a9233be68fb63328c04b0614b4290 to your computer and use it in GitHub Desktop.
OECU Moodle 授業絞り込みスクリプト
// ==UserScript==
// @name Moodle 授業絞り込み (時間割枠 常時表示版)
// @namespace https://gist.github.com/Hiryuto-oecu/d02a9233be68fb63328c04b0614b4290
// @version 1.7.0
// @description Moodleのコース一覧で、1限〜5限のアコーディオン枠を常に表示し、現在の時間帯のみ自動展開。集中講義も一番下に常時表示します。
// @author Hiryuto
// @match https://moodle2026.mc2.osakac.ac.jp/2026/my/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// --- 設定 ---
const gridContainerSelector = '.card-grid[data-region="card-deck"]';
const courseCardWrapperSelector = 'div.col.d-flex';
const contentRegionSelector = '#block-region-content';
const timeoutDuration = 30000; // 30秒
// 時間割の定義 (開始・終了を分単位で計算して判定)
const timeTable = {
1: { start: 9 * 60 + 0, end: 10 * 60 + 45, text: "9:00 〜 10:45" },
2: { start: 10 * 60 + 55, end: 12 * 60 + 40, text: "10:55 〜 12:40" },
3: { start: 13 * 60 + 25, end: 15 * 60 + 10, text: "13:25 〜 15:10" },
4: { start: 15 * 60 + 20, end: 17 * 60 + 5, text: "15:20 〜 17:05" },
5: { start: 17 * 60 + 15, end: 19 * 60 + 0, text: "17:15 〜 19:00" }
};
/**
* 現在の時間が何限目かを判定する
* 該当しない時間帯(休み時間や授業後など)は null を返す
*/
function getCurrentPeriod() {
const now = new Date();
const minutes = now.getHours() * 60 + now.getMinutes();
for (const [period, times] of Object.entries(timeTable)) {
if (minutes >= times.start && minutes <= times.end) {
return parseInt(period, 10);
}
}
return null;
}
/**
* メインの処理:コースをフィルタリングし、DOMを再構築する
*/
function processCourses() {
const originalGrid = document.querySelector(gridContainerSelector);
const mainContent = document.querySelector(contentRegionSelector);
if (!originalGrid || !mainContent) {
console.error('Tampermonkey: 必須要素が見つかりませんでした。');
return;
}
if (document.querySelector('#today-courses-container')) {
return; // 既に処理済みなら何もしない
}
const today = new Date();
const dayOfWeek = ['日曜', '月曜', '火曜', '水曜', '木曜', '金曜', '土曜'][today.getDay()];
const todayChar = dayOfWeek.charAt(0);
const currentMonth = today.getMonth() + 1;
// --- 1. 学期フィルタの判定と実行 ---
let isSemesterPeriod = false;
let semesterKeywordsToHide = [];
if (currentMonth >= 4 && currentMonth <= 7) {
isSemesterPeriod = true;
semesterKeywordsToHide = ["後期", "後期前半", "後期後半"];
} else if (currentMonth >= 10 || currentMonth === 1) {
isSemesterPeriod = true;
semesterKeywordsToHide = ["前期", "前期前半", "前期後半"];
}
const allCourseWrappers = originalGrid.querySelectorAll(courseCardWrapperSelector);
if (isSemesterPeriod) {
allCourseWrappers.forEach(wrapper => {
const titleElement = wrapper.querySelector('.multiline');
if (!titleElement) return;
const title = titleElement.getAttribute('title');
if (semesterKeywordsToHide.some(keyword => title.includes(keyword))) {
wrapper.style.display = 'none';
}
});
}
// --- 2. 「本日の授業」の絞り込みと時限の取得、および「集中講義」の抽出 ---
const groupedCourses = {};
const intensiveCourses = [];
allCourseWrappers.forEach(wrapper => {
if (wrapper.style.display === 'none') return;
const titleElement = wrapper.querySelector('.multiline');
if (!titleElement) return;
const originalTitle = titleElement.getAttribute('title') || titleElement.textContent;
const normalizedTitle = originalTitle.replace(/[0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0));
// 「集中」が含まれている場合は専用の配列へ入れる
if (normalizedTitle.includes('集中')) {
intensiveCourses.push({ wrapper });
return;
}
// タイトルに含まれる曜日を抽出
const daysInTitle = normalizedTitle.match(/(月|火|水|木|金|土|日)曜?/g) || [];
const isTodayCourse = daysInTitle.some(day => dayOfWeek.startsWith(day.replace('曜','')));
if (isTodayCourse) {
let sortPeriod = 99; // 時間指定なし
let displayPeriod = "";
// 今日の曜日に対する時限を抽出
const periodMatch = normalizedTitle.match(new RegExp(`${todayChar}曜?([\\d・\\-~〜,]+)`));
if (periodMatch) {
displayPeriod = periodMatch[1].replace(/[・\-~〜,]+$/, '');
const firstNumMatch = displayPeriod.match(/\d+/);
if (firstNumMatch) {
sortPeriod = parseInt(firstNumMatch[0], 10);
}
}
if (!groupedCourses[sortPeriod]) {
groupedCourses[sortPeriod] = [];
}
groupedCourses[sortPeriod].push({ wrapper, displayPeriod });
}
});
// --- 3. UI(アコーディオン)の構築 ---
const newHeader = document.createElement('h2');
newHeader.textContent = `本日の授業 (${dayOfWeek})`;
newHeader.style.marginBottom = "1rem";
const container = document.createElement('div');
container.id = 'today-courses-container';
container.style.marginBottom = '2rem';
const currentPeriod = getCurrentPeriod();
// 1限〜5限を固定で表示
const fixedPeriods = [1, 2, 3, 4, 5];
fixedPeriods.forEach(period => {
const courses = groupedCourses[period] || [];
const details = document.createElement('details');
details.style.marginBottom = '0.75rem';
details.style.border = '1px solid #ced4da';
details.style.borderRadius = '0.25rem';
details.style.backgroundColor = '#f8f9fa';
// 開く条件の判定 (現在の時間帯なら授業がなくても開く)
let isOpen = false;
if (period === currentPeriod) {
isOpen = true;
} else if (currentPeriod !== null && courses.length > 0) {
isOpen = courses.some(data => data.displayPeriod && data.displayPeriod.includes(currentPeriod.toString()));
}
details.open = isOpen;
const summary = document.createElement('summary');
summary.style.cursor = 'pointer';
summary.style.padding = '0.75rem 1rem';
summary.style.fontWeight = 'bold';
summary.style.fontSize = '1.1rem';
summary.style.outline = 'none';
let summaryHtml = `${period}限 (${timeTable[period].text})`;
// 現在の時間帯にはバッジを付ける
if (isOpen && currentPeriod !== null && period === currentPeriod) {
summaryHtml += ' <span class="badge" style="background-color:#dc3545; color:white; margin-left:10px;">現在開講中</span>';
}
summary.innerHTML = summaryHtml;
details.appendChild(summary);
const grid = document.createElement('div');
grid.style.padding = '1rem';
grid.style.backgroundColor = '#fff';
grid.style.borderTop = '1px solid #ced4da';
grid.style.borderBottomLeftRadius = '0.25rem';
grid.style.borderBottomRightRadius = '0.25rem';
if (courses.length > 0) {
grid.className = 'card-grid mx-0 row row-cols-1 row-cols-sm-2 row-cols-lg-3';
courses.forEach(data => {
grid.appendChild(data.wrapper.cloneNode(true));
});
} else {
// 授業がない場合の表示
grid.style.color = '#6c757d';
grid.textContent = 'この時間帯の授業はありません。';
}
details.appendChild(grid);
container.appendChild(details);
});
// 曜日指定で時限が取得できなかったもの (period = 99) があれば表示
if (groupedCourses[99] && groupedCourses[99].length > 0) {
const details99 = document.createElement('details');
details99.style.marginBottom = '0.75rem';
details99.style.border = '1px solid #ced4da';
details99.style.borderRadius = '0.25rem';
details99.style.backgroundColor = '#f8f9fa';
const summary99 = document.createElement('summary');
summary99.style.cursor = 'pointer';
summary99.style.padding = '0.75rem 1rem';
summary99.style.fontWeight = 'bold';
summary99.style.fontSize = '1.1rem';
summary99.textContent = `時間指定なし / その他`;
details99.appendChild(summary99);
const grid99 = document.createElement('div');
grid99.className = 'card-grid mx-0 row row-cols-1 row-cols-sm-2 row-cols-lg-3';
grid99.style.padding = '1rem';
grid99.style.backgroundColor = '#fff';
grid99.style.borderTop = '1px solid #ced4da';
groupedCourses[99].forEach(data => {
grid99.appendChild(data.wrapper.cloneNode(true));
});
details99.appendChild(grid99);
container.appendChild(details99);
}
// --- 3-2. 集中講義の表示(常に一番下に枠を表示) ---
const intensiveDetails = document.createElement('details');
intensiveDetails.style.marginBottom = '0.75rem';
intensiveDetails.style.border = '1px solid #ced4da';
intensiveDetails.style.borderRadius = '0.25rem';
intensiveDetails.style.backgroundColor = '#eef3fc';
const intensiveSummary = document.createElement('summary');
intensiveSummary.style.cursor = 'pointer';
intensiveSummary.style.padding = '0.75rem 1rem';
intensiveSummary.style.fontWeight = 'bold';
intensiveSummary.style.fontSize = '1.1rem';
intensiveSummary.innerHTML = `集中講義`;
intensiveDetails.appendChild(intensiveSummary);
const intensiveGrid = document.createElement('div');
intensiveGrid.style.padding = '1rem';
intensiveGrid.style.backgroundColor = '#fff';
intensiveGrid.style.borderTop = '1px solid #ced4da';
intensiveGrid.style.borderBottomLeftRadius = '0.25rem';
intensiveGrid.style.borderBottomRightRadius = '0.25rem';
if (intensiveCourses.length > 0) {
intensiveGrid.className = 'card-grid mx-0 row row-cols-1 row-cols-sm-2 row-cols-lg-3';
intensiveCourses.forEach(data => {
intensiveGrid.appendChild(data.wrapper.cloneNode(true));
});
} else {
// 集中講義がない場合の表示
intensiveGrid.style.color = '#6c757d';
intensiveGrid.textContent = '現在登録されている集中講義はありません。';
}
intensiveDetails.appendChild(intensiveGrid);
container.appendChild(intensiveDetails);
// Moodleのメインエリアの上部に追加
mainContent.parentNode.insertBefore(newHeader, mainContent);
mainContent.parentNode.insertBefore(container, mainContent);
}
/**
* 指定された要素が表示されるまで待機する
*/
function waitForElement() {
const observer = new MutationObserver((mutations, obs) => {
const gridContainer = document.querySelector(gridContainerSelector);
if (gridContainer && gridContainer.querySelector(courseCardWrapperSelector)) {
processCourses();
obs.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
if (document.querySelector('#today-courses-container') === null) {
observer.disconnect();
}
}, timeoutDuration);
}
// スクリプトの実行を開始
const initialGrid = document.querySelector(gridContainerSelector);
if (initialGrid && initialGrid.querySelector(courseCardWrapperSelector)) {
processCourses();
} else {
waitForElement();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment