Created
April 13, 2026 01:01
-
-
Save Hiryuto-oecu/d02a9233be68fb63328c04b0614b4290 to your computer and use it in GitHub Desktop.
OECU Moodle 授業絞り込みスクリプト
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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