doomsday.git

doomsday

~
git clone https://github.com/debuman/doomsday.git
commit 915f0ee7e2265b61e131b6e51deffcda36efde31
Author: devastator-x <devastator-x@users.noreply.github.com>
Date: Sun, 1 Feb 2026 16:59:19 +0900

Initial commit: Doomsday GNOME Extension v1.0

Features:
- D-Day countdown display in GNOME panel
- Multiple event management with add/edit/delete
- Quick add/edit/delete from extension menu
- Panel position customization
- Automatic daily updates at midnight
- Preferences UI with About page

Files included:
- extension.js: Main extension logic with custom menu items
- prefs.js: Preferences UI with General and About pages
- metadata.json: Extension metadata
- stylesheet.css: Custom styling
- schemas/: GSettings schema
- README.txt: Project documentation
- INSTALL.md: Installation guide
- LICENSE: GPL-3.0
- install.sh: Installation script

Diffstat:
A.gitignore | 22++++++++++++++++++++++
AINSTALL.md | 39+++++++++++++++++++++++++++++++++++++++
ALICENSE | 17+++++++++++++++++
AREADME.txt | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aextension.js | 577+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainstall.sh | 30++++++++++++++++++++++++++++++
Ametadata.json | 9+++++++++
Aprefs.js | 447+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aschemas/org.gnome.shell.extensions.doomsday.gschema.xml | 39+++++++++++++++++++++++++++++++++++++++
Astylesheet.css | 47+++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1390 insertion(+), 0 deletion(-)

diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,22 @@
+# Compiled schema
+schemas/gschemas.compiled
+
+# ZIP package
+*.shell-extension.zip
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.claude/
+
+# System
+.DS_Store
+Thumbs.db
+
+# Temporary files
+*.tmp
+*.bak
+*.log
diff --git a/INSTALL.md b/INSTALL.md
@@ -0,0 +1,39 @@
+# Doomsday Extension 설치 방법
+
+## 자동 설치 (권장)
+
+1. [Releases](https://github.com/devastator-x/doomsday/releases)에서 최신 버전의 ZIP 파일 다운로드
+2. Extension Manager 실행
+3. "Install from file..." 선택
+4. 다운로드한 ZIP 파일 선택
+5. Extension 활성화
+
+## 수동 설치
+
+```bash
+# 1. ZIP 파일 다운로드
+wget https://github.com/devastator-x/doomsday/releases/latest/download/doomsday@devastator-x.github.io.shell-extension.zip
+
+# 2. Extension 디렉토리에 압축 해제
+mkdir -p ~/.local/share/gnome-shell/extensions/doomsday@devastator-x.github.io
+unzip doomsday@devastator-x.github.io.shell-extension.zip -d ~/.local/share/gnome-shell/extensions/doomsday@devastator-x.github.io
+
+# 3. 스키마 컴파일
+glib-compile-schemas ~/.local/share/gnome-shell/extensions/doomsday@devastator-x.github.io/schemas/
+
+# 4. Extension 활성화
+gnome-extensions enable doomsday@devastator-x.github.io
+
+# 5. GNOME Shell 재시작 (Alt+F2, 'r', Enter)
+```
+
+## Git에서 직접 설치 (개발자용)
+
+```bash
+# 1. 저장소 클론
+git clone https://github.com/devastator-x/doomsday.git
+cd doomsday
+
+# 2. 설치 스크립트 실행
+./install.sh
+```
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,17 @@
+GNU GENERAL PUBLIC LICENSE
+Version 3, 29 June 2007
+
+Copyright (C) 2025 devastator-x
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/README.txt b/README.txt
@@ -0,0 +1,163 @@
+================================================================================
+  Doomsday - GNOME Extension
+  D-Day 카운트다운 GNOME 패널 확장 프로그램
+================================================================================
+
+GNOME Version: 45, 46, 47
+License: GPL-3.0
+
+--------------------------------------------------------------------------------
+주요 기능
+--------------------------------------------------------------------------------
+
+✓ GNOME 패널에 D-Day 표시
+  - 중요한 이벤트까지 남은 날짜를 실시간으로 확인
+
+✓ 여러 D-Day 관리
+  - 무제한 이벤트 추가 및 관리
+
+✓ 빠른 추가/수정/삭제
+  - 메뉴에서 바로 D-Day를 추가, 수정, 삭제 가능
+
+✓ 이벤트 선택
+  - 클릭 한 번으로 표시할 이벤트 전환
+
+✓ 패널 위치 설정
+  - 좌측, 중앙, 우측 중 선택 가능
+
+✓ 자동 업데이트
+  - 매일 자정에 자동으로 카운트다운 갱신
+
+--------------------------------------------------------------------------------
+설치 방법
+--------------------------------------------------------------------------------
+
+[방법 1] Extension Manager 사용 (권장)
+
+1. Extension Manager 앱 실행
+2. "Install from file..." 선택
+3. doomsday@devastator-x.github.io.shell-extension.zip 선택
+4. Extension 활성화
+
+[방법 2] 수동 설치
+
+# 저장소 클론
+git clone https://github.com/devastator-x/doomsday.git
+cd doomsday
+
+# 설치 스크립트 실행
+./install.sh
+
+# GNOME Shell 재시작 (Alt+F2, 'r', Enter)
+
+[방법 3] ZIP 파일로 설치
+
+# Extension 디렉토리에 압축 해제
+mkdir -p ~/.local/share/gnome-shell/extensions/doomsday@devastator-x.github.io
+unzip doomsday@devastator-x.github.io.shell-extension.zip \
+  -d ~/.local/share/gnome-shell/extensions/doomsday@devastator-x.github.io
+
+# 스키마 컴파일
+glib-compile-schemas \
+  ~/.local/share/gnome-shell/extensions/doomsday@devastator-x.github.io/schemas/
+
+# Extension 활성화
+gnome-extensions enable doomsday@devastator-x.github.io
+
+# GNOME Shell 재시작 (Alt+F2, 'r', Enter)
+
+--------------------------------------------------------------------------------
+사용법
+--------------------------------------------------------------------------------
+
+[D-Day 추가]
+
+1. 패널의 D-Day 클릭
+2. "➕ Add D-Day" 선택
+3. 이벤트 이름과 날짜(YYYY-MM-DD) 입력
+4. "Add" 클릭
+
+[D-Day 수정/삭제]
+
+• 메뉴에서: 각 D-Day 항목 우측의 ✏️(수정) 또는 🗑️(삭제) 버튼 클릭
+• 설정에서: "⚙️ Manage Events" → Edit/Delete 버튼 사용
+
+[표시할 D-Day 선택]
+
+• 메뉴에서 원하는 D-Day 클릭 (점(•)으로 선택된 항목 표시)
+
+[패널 위치 변경]
+
+1. "⚙️ Manage Events" 클릭
+2. "Panel Settings"에서 위치와 인덱스 조정
+
+--------------------------------------------------------------------------------
+배포 방법
+--------------------------------------------------------------------------------
+
+[GNOME Extensions 웹사이트에 업로드]
+
+1. extensions.gnome.org 계정 생성
+2. 로그인 후 "Upload Extension" 클릭
+3. doomsday@devastator-x.github.io.shell-extension.zip 업로드
+4. 심사 대기 (1-2주 소요)
+5. 승인 후 자동 배포
+
+[GitHub Releases로 배포]
+
+1. GitHub 저장소 생성
+2. Releases에 ZIP 파일 업로드
+3. 사용자들이 직접 다운로드하여 설치
+
+[로컬/직접 배포]
+
+• ZIP 파일을 직접 공유하여 수동 설치
+
+--------------------------------------------------------------------------------
+개발
+--------------------------------------------------------------------------------
+
+# 개발 모드로 설치
+git clone https://github.com/devastator-x/doomsday.git
+cd doomsday
+./install.sh
+
+# 로그 확인
+journalctl -f -o cat /usr/bin/gnome-shell
+
+# Extension 재시작 (Alt+F2, 'r', Enter)
+
+--------------------------------------------------------------------------------
+파일 구조
+--------------------------------------------------------------------------------
+
+doomsday@devastator-x.github.io/
+├── extension.js                 # 메인 extension 로직
+├── prefs.js                     # 설정 UI
+├── metadata.json                # Extension 메타데이터
+├── stylesheet.css               # 스타일
+├── schemas/                     # GSettings 스키마
+│   └── org.gnome.shell.extensions.doomsday.gschema.xml
+├── install.sh                   # 설치 스크립트
+└── README.txt                   # 이 파일
+
+--------------------------------------------------------------------------------
+라이선스
+--------------------------------------------------------------------------------
+
+GPL-3.0 License
+자세한 내용은 LICENSE 파일을 참고하세요.
+
+--------------------------------------------------------------------------------
+기여
+--------------------------------------------------------------------------------
+
+버그 리포트와 기능 제안은 GitHub Issues에 올려주세요!
+https://github.com/devastator-x/doomsday/issues
+
+--------------------------------------------------------------------------------
+
+Made with ❤️ by devastator-x
+https://github.com/devastator-x
+
+================================================================================
diff --git a/extension.js b/extension.js
@@ -0,0 +1,577 @@
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import GLib from 'gi://GLib';
+import Clutter from 'gi://Clutter';
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js';
+
+// Custom menu item with edit and delete buttons
+const DoomsdayMenuItem = GObject.registerClass(
+class DoomsdayMenuItem extends PopupMenu.PopupBaseMenuItem {
+    _init(indicator, event, displayText, isSelected) {
+        super._init();
+
+        this._indicator = indicator;
+        this._event = event;
+
+        // Create horizontal box layout
+        const box = new St.BoxLayout({
+            vertical: false,
+            x_expand: true,
+            style_class: 'doomsday-menu-item-box',
+        });
+
+        // Event label
+        this._label = new St.Label({
+            text: displayText,
+            y_align: Clutter.ActorAlign.CENTER,
+            x_expand: true,
+        });
+        box.add_child(this._label);
+
+        // Mark selected item with a dot
+        if (isSelected) {
+            this.setOrnament(PopupMenu.Ornament.DOT);
+        }
+
+        // Edit button
+        const editButton = new St.Button({
+            style_class: 'doomsday-edit-button',
+            child: new St.Icon({
+                icon_name: 'document-edit-symbolic',
+                icon_size: 16,
+            }),
+            x_align: Clutter.ActorAlign.END,
+        });
+        editButton.connect('clicked', () => {
+            this._indicator._showEditDialog(this._event);
+            return Clutter.EVENT_STOP;
+        });
+        box.add_child(editButton);
+
+        // Delete button
+        const deleteButton = new St.Button({
+            style_class: 'doomsday-delete-button',
+            child: new St.Icon({
+                icon_name: 'user-trash-symbolic',
+                icon_size: 16,
+            }),
+            x_align: Clutter.ActorAlign.END,
+        });
+        deleteButton.connect('clicked', () => {
+            this._indicator._showDeleteDialog(this._event);
+            return Clutter.EVENT_STOP;
+        });
+        box.add_child(deleteButton);
+
+        this.add_child(box);
+
+        // Click on the item itself to select it
+        this.connect('activate', () => {
+            this._indicator._settings.set_string('selected-event-id', this._event.id);
+        });
+    }
+});
+
+const DoomsdayIndicator = GObject.registerClass(
+class DoomsdayIndicator extends PanelMenu.Button {
+    _init(extension) {
+        super._init(0.0, 'Doomsday Indicator', false);
+
+        this._extension = extension;
+        this._settings = extension.getSettings();
+
+        // Create label for panel
+        this._label = new St.Label({
+            text: 'D-Day',
+            y_align: Clutter.ActorAlign.CENTER,
+            style_class: 'doomsday-label'
+        });
+        this.add_child(this._label);
+
+        // Build popup menu
+        this._buildMenu();
+
+        // Connect to settings changes
+        this._settingsChangedId = this._settings.connect('changed',
+            this._onSettingsChanged.bind(this));
+
+        // Initial update
+        this._updateDisplay();
+
+        // Setup daily timer
+        this._setupTimer();
+    }
+
+    _buildMenu() {
+        // Add "Open Preferences" menu item at the bottom
+        // Menu items for D-Days will be added dynamically in _updateMenuItems
+    }
+
+    _updateDisplay() {
+        // Get selected event from settings
+        const selectedId = this._settings.get_string('selected-event-id');
+        const eventsJson = this._settings.get_string('dday-events');
+
+        try {
+            const events = JSON.parse(eventsJson);
+            const selectedEvent = events.find(e => e.id === selectedId);
+
+            if (selectedEvent) {
+                const displayText = this._calculateDDay(
+                    selectedEvent.name,
+                    selectedEvent.date
+                );
+                this._label.set_text(displayText);
+            } else {
+                this._label.set_text('No D-Day');
+            }
+
+            // Update menu items
+            this._updateMenuItems(events, selectedId);
+
+        } catch (e) {
+            console.error('Doomsday: Failed to parse D-Day events:', e);
+            this._label.set_text('Error');
+        }
+    }
+
+    _calculateDDay(name, targetDateStr) {
+        // Parse target date (YYYY-MM-DD format)
+        const [year, month, day] = targetDateStr.split('-').map(Number);
+        const targetDate = new Date(year, month - 1, day);
+
+        // Get today at midnight (ignore time component)
+        const today = new Date();
+        today.setHours(0, 0, 0, 0);
+
+        // Calculate difference in days
+        const diffTime = targetDate - today;
+        const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
+
+        // Format according to requirements
+        if (diffDays === 0) {
+            return `${name} D-Day`;
+        } else if (diffDays > 0) {
+            return `${name} D-${diffDays}`;
+        } else {
+            return `${name} D+${Math.abs(diffDays)}`;
+        }
+    }
+
+    _setupTimer() {
+        // Calculate seconds until next midnight
+        const now = new Date();
+        const tomorrow = new Date(now);
+        tomorrow.setDate(tomorrow.getDate() + 1);
+        tomorrow.setHours(0, 0, 0, 0);
+        const secondsUntilMidnight = Math.floor((tomorrow - now) / 1000);
+
+        // Set up initial timeout to midnight
+        this._midnightTimeoutId = GLib.timeout_add_seconds(
+            GLib.PRIORITY_DEFAULT,
+            secondsUntilMidnight,
+            () => {
+                this._updateDisplay();
+                // After first midnight, update every 24 hours
+                this._dailyTimeoutId = GLib.timeout_add_seconds(
+                    GLib.PRIORITY_DEFAULT,
+                    86400, // 24 hours
+                    () => {
+                        this._updateDisplay();
+                        return GLib.SOURCE_CONTINUE;
+                    }
+                );
+                return GLib.SOURCE_REMOVE;
+            }
+        );
+    }
+
+    _onSettingsChanged() {
+        this._updateDisplay();
+    }
+
+    _updateMenuItems(events, selectedId) {
+        // Clear existing menu items
+        this.menu.removeAll();
+
+        // Add custom menu items for each D-Day event with edit/delete buttons
+        events.forEach(event => {
+            const displayText = this._calculateDDay(event.name, event.date);
+            const item = new DoomsdayMenuItem(
+                this,
+                event,
+                displayText,
+                event.id === selectedId
+            );
+            this.menu.addMenuItem(item);
+        });
+
+        // Add separator
+        if (events.length > 0) {
+            this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+        }
+
+        // Add "Add D-Day" menu item
+        const addItem = new PopupMenu.PopupMenuItem('➕ Add D-Day');
+        addItem.connect('activate', () => {
+            this._showAddDialog();
+        });
+        this.menu.addMenuItem(addItem);
+
+        // Add "Manage Events" menu item that opens preferences
+        const manageItem = new PopupMenu.PopupMenuItem('⚙️ Manage Events');
+        manageItem.connect('activate', () => {
+            this._extension.openPreferences();
+        });
+        this.menu.addMenuItem(manageItem);
+    }
+
+    _showAddDialog() {
+        const dialog = new ModalDialog.ModalDialog();
+
+        // Title
+        const title = new St.Label({
+            text: 'Add D-Day Event',
+            style_class: 'doomsday-dialog-title',
+            x_align: Clutter.ActorAlign.CENTER,
+        });
+        dialog.contentLayout.add_child(title);
+
+        // Name entry
+        const nameBox = new St.BoxLayout({
+            vertical: false,
+            style_class: 'doomsday-dialog-row',
+        });
+        const nameLabel = new St.Label({
+            text: 'Name: ',
+            style_class: 'doomsday-dialog-label',
+            y_align: Clutter.ActorAlign.CENTER,
+        });
+        const nameEntry = new St.Entry({
+            hint_text: 'Event name',
+            style_class: 'doomsday-dialog-entry',
+            can_focus: true,
+            x_expand: true,
+        });
+        nameBox.add_child(nameLabel);
+        nameBox.add_child(nameEntry);
+        dialog.contentLayout.add_child(nameBox);
+
+        // Date entry
+        const dateBox = new St.BoxLayout({
+            vertical: false,
+            style_class: 'doomsday-dialog-row',
+        });
+        const dateLabel = new St.Label({
+            text: 'Date: ',
+            style_class: 'doomsday-dialog-label',
+            y_align: Clutter.ActorAlign.CENTER,
+        });
+        const today = new Date();
+        const dateStr = today.toISOString().split('T')[0];
+        const dateEntry = new St.Entry({
+            text: dateStr,
+            hint_text: 'YYYY-MM-DD',
+            style_class: 'doomsday-dialog-entry',
+            can_focus: true,
+            x_expand: true,
+        });
+        dateBox.add_child(dateLabel);
+        dateBox.add_child(dateEntry);
+        dialog.contentLayout.add_child(dateBox);
+
+        // Buttons
+        dialog.addButton({
+            label: 'Cancel',
+            action: () => dialog.close(),
+            key: Clutter.KEY_Escape,
+        });
+
+        dialog.addButton({
+            label: 'Add',
+            action: () => {
+                const newName = nameEntry.get_text().trim();
+                const newDate = dateEntry.get_text().trim();
+
+                // Validate inputs
+                if (!newName || !newDate) {
+                    return;
+                }
+
+                if (!/^\d{4}-\d{2}-\d{2}$/.test(newDate)) {
+                    return;
+                }
+
+                // Add new event
+                this._addEvent(newName, newDate);
+                dialog.close();
+            },
+            default: true,
+        });
+
+        dialog.open();
+    }
+
+    _showEditDialog(event) {
+        const dialog = new ModalDialog.ModalDialog();
+
+        // Title
+        const title = new St.Label({
+            text: 'Edit D-Day Event',
+            style_class: 'doomsday-dialog-title',
+            x_align: Clutter.ActorAlign.CENTER,
+        });
+        dialog.contentLayout.add_child(title);
+
+        // Name entry
+        const nameBox = new St.BoxLayout({
+            vertical: false,
+            style_class: 'doomsday-dialog-row',
+        });
+        const nameLabel = new St.Label({
+            text: 'Name: ',
+            style_class: 'doomsday-dialog-label',
+            y_align: Clutter.ActorAlign.CENTER,
+        });
+        const nameEntry = new St.Entry({
+            text: event.name,
+            style_class: 'doomsday-dialog-entry',
+            can_focus: true,
+            x_expand: true,
+        });
+        nameBox.add_child(nameLabel);
+        nameBox.add_child(nameEntry);
+        dialog.contentLayout.add_child(nameBox);
+
+        // Date entry
+        const dateBox = new St.BoxLayout({
+            vertical: false,
+            style_class: 'doomsday-dialog-row',
+        });
+        const dateLabel = new St.Label({
+            text: 'Date: ',
+            style_class: 'doomsday-dialog-label',
+            y_align: Clutter.ActorAlign.CENTER,
+        });
+        const dateEntry = new St.Entry({
+            text: event.date,
+            hint_text: 'YYYY-MM-DD',
+            style_class: 'doomsday-dialog-entry',
+            can_focus: true,
+            x_expand: true,
+        });
+        dateBox.add_child(dateLabel);
+        dateBox.add_child(dateEntry);
+        dialog.contentLayout.add_child(dateBox);
+
+        // Buttons
+        dialog.addButton({
+            label: 'Cancel',
+            action: () => dialog.close(),
+            key: Clutter.KEY_Escape,
+        });
+
+        dialog.addButton({
+            label: 'Save',
+            action: () => {
+                const newName = nameEntry.get_text().trim();
+                const newDate = dateEntry.get_text().trim();
+
+                // Validate inputs
+                if (!newName || !newDate) {
+                    return;
+                }
+
+                if (!/^\d{4}-\d{2}-\d{2}$/.test(newDate)) {
+                    return;
+                }
+
+                // Update event
+                this._updateEvent(event.id, newName, newDate);
+                dialog.close();
+            },
+            default: true,
+        });
+
+        dialog.open();
+    }
+
+    _showDeleteDialog(event) {
+        const dialog = new ModalDialog.ModalDialog();
+
+        // Message
+        const message = new St.Label({
+            text: `Delete "${event.name}"?\nThis action cannot be undone.`,
+            style_class: 'doomsday-dialog-message',
+            x_align: Clutter.ActorAlign.CENTER,
+        });
+        dialog.contentLayout.add_child(message);
+
+        // Buttons
+        dialog.addButton({
+            label: 'Cancel',
+            action: () => dialog.close(),
+            key: Clutter.KEY_Escape,
+        });
+
+        dialog.addButton({
+            label: 'Delete',
+            action: () => {
+                this._deleteEvent(event.id);
+                dialog.close();
+            },
+        });
+
+        dialog.open();
+    }
+
+    _addEvent(name, date) {
+        const eventsJson = this._settings.get_string('dday-events');
+        let events = [];
+
+        try {
+            events = JSON.parse(eventsJson);
+        } catch (e) {
+            console.error('Doomsday: Failed to parse events:', e);
+            events = [];
+        }
+
+        // Create new event with UUID
+        const newId = GLib.uuid_string_random();
+        events.push({ id: newId, name, date });
+
+        // Auto-select if it's the first event
+        if (events.length === 1) {
+            this._settings.set_string('selected-event-id', newId);
+        }
+
+        // Save
+        this._settings.set_string('dday-events', JSON.stringify(events));
+    }
+
+    _updateEvent(eventId, newName, newDate) {
+        const eventsJson = this._settings.get_string('dday-events');
+        let events = [];
+
+        try {
+            events = JSON.parse(eventsJson);
+        } catch (e) {
+            console.error('Doomsday: Failed to parse events:', e);
+            return;
+        }
+
+        // Update event
+        const index = events.findIndex(e => e.id === eventId);
+        if (index !== -1) {
+            events[index] = { id: eventId, name: newName, date: newDate };
+            this._settings.set_string('dday-events', JSON.stringify(events));
+        }
+    }
+
+    _deleteEvent(eventId) {
+        const eventsJson = this._settings.get_string('dday-events');
+        let events = [];
+
+        try {
+            events = JSON.parse(eventsJson);
+        } catch (e) {
+            console.error('Doomsday: Failed to parse events:', e);
+            return;
+        }
+
+        // Remove event
+        events = events.filter(e => e.id !== eventId);
+
+        // If deleted event was selected, select first or clear
+        const selectedId = this._settings.get_string('selected-event-id');
+        if (selectedId === eventId) {
+            const newSelectedId = events.length > 0 ? events[0].id : '';
+            this._settings.set_string('selected-event-id', newSelectedId);
+        }
+
+        // Save
+        this._settings.set_string('dday-events', JSON.stringify(events));
+    }
+
+    destroy() {
+        // Clean up timers
+        if (this._midnightTimeoutId) {
+            GLib.Source.remove(this._midnightTimeoutId);
+            this._midnightTimeoutId = null;
+        }
+        if (this._dailyTimeoutId) {
+            GLib.Source.remove(this._dailyTimeoutId);
+            this._dailyTimeoutId = null;
+        }
+
+        // Disconnect settings
+        if (this._settingsChangedId) {
+            this._settings.disconnect(this._settingsChangedId);
+            this._settingsChangedId = null;
+        }
+
+        super.destroy();
+    }
+});
+
+export default class DoomsdayExtension extends Extension {
+    enable() {
+        console.debug(`enabling ${this.metadata.name}`);
+
+        this._settings = this.getSettings();
+        this._indicator = new DoomsdayIndicator(this);
+        this._addIndicatorToPanel();
+
+        // Watch for panel position changes
+        this._positionChangedId = this._settings.connect('changed::panel-position', () => {
+            this._repositionIndicator();
+        });
+
+        // Watch for panel index changes
+        this._indexChangedId = this._settings.connect('changed::panel-index', () => {
+            this._repositionIndicator();
+        });
+    }
+
+    disable() {
+        console.debug(`disabling ${this.metadata.name}`);
+
+        if (this._positionChangedId) {
+            this._settings.disconnect(this._positionChangedId);
+            this._positionChangedId = null;
+        }
+
+        if (this._indexChangedId) {
+            this._settings.disconnect(this._indexChangedId);
+            this._indexChangedId = null;
+        }
+
+        if (this._indicator) {
+            this._indicator.destroy();
+            this._indicator = null;
+        }
+
+        this._settings = null;
+    }
+
+    _addIndicatorToPanel() {
+        const position = this._settings.get_string('panel-position');
+        const index = this._settings.get_int('panel-index');
+        const box = position === 'left' ? 'left' : position === 'center' ? 'center' : 'right';
+
+        Main.panel.addToStatusArea(this.uuid, this._indicator, index, box);
+    }
+
+    _repositionIndicator() {
+        if (this._indicator) {
+            // Destroy and recreate the indicator
+            this._indicator.destroy();
+            this._indicator = new DoomsdayIndicator(this);
+            this._addIndicatorToPanel();
+        }
+    }
+}
diff --git a/install.sh b/install.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Doomsday Extension 설치 스크립트
+
+EXTENSION_UUID="doomsday@devastator-x.github.io"
+EXTENSION_DIR="$HOME/.local/share/gnome-shell/extensions/$EXTENSION_UUID"
+
+echo "Doomsday Extension 설치 중..."
+
+# Extension 디렉토리 생성
+mkdir -p "$EXTENSION_DIR"
+
+# 파일 복사
+cp extension.js "$EXTENSION_DIR/"
+cp prefs.js "$EXTENSION_DIR/"
+cp metadata.json "$EXTENSION_DIR/"
+cp stylesheet.css "$EXTENSION_DIR/"
+cp -r schemas "$EXTENSION_DIR/"
+
+# 스키마 컴파일
+echo "스키마 컴파일 중..."
+glib-compile-schemas "$EXTENSION_DIR/schemas/"
+
+# Extension 활성화
+echo "Extension 활성화 중..."
+gnome-extensions enable "$EXTENSION_UUID"
+
+echo "설치 완료!"
+echo "GNOME Shell을 재시작하려면: Alt+F2를 누르고 'r'을 입력하세요"
+echo "(Wayland 사용 시: 로그아웃 후 재로그인)"
diff --git a/metadata.json b/metadata.json
@@ -0,0 +1,9 @@
+{
+  "uuid": "doomsday@devastator-x.github.io",
+  "name": "Doomsday",
+  "description": "Display D-Day countdowns in the top panel bar. Track important events with customizable countdown timers.",
+  "shell-version": ["45", "46", "47"],
+  "url": "https://github.com/devastator-x",
+  "settings-schema": "org.gnome.shell.extensions.doomsday",
+  "version": 1
+}
diff --git a/prefs.js b/prefs.js
@@ -0,0 +1,447 @@
+import Gtk from 'gi://Gtk?version=4.0';
+import Adw from 'gi://Adw';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+export default class DoomsdayPreferences extends ExtensionPreferences {
+    fillPreferencesWindow(window) {
+        // Create a preferences page
+        const page = new Adw.PreferencesPage({
+            title: 'General',
+            icon_name: 'dialog-information-symbolic',
+        });
+        window.add(page);
+
+        // Create a preferences group for panel settings
+        const panelGroup = new Adw.PreferencesGroup({
+            title: 'Panel Settings',
+            description: 'Customize the extension appearance',
+        });
+        page.add(panelGroup);
+
+        // Panel position selector
+        this._settings = this.getSettings();
+        const positionRow = new Adw.ComboRow({
+            title: 'Panel Position',
+            subtitle: 'Choose where the extension appears in the top panel',
+        });
+
+        const positionModel = new Gtk.StringList();
+        positionModel.append('Left');
+        positionModel.append('Center');
+        positionModel.append('Right');
+        positionRow.set_model(positionModel);
+
+        // Set initial value
+        const currentPosition = this._settings.get_string('panel-position');
+        const positions = ['left', 'center', 'right'];
+        positionRow.set_selected(positions.indexOf(currentPosition));
+
+        // Connect to changes
+        positionRow.connect('notify::selected', () => {
+            const newPosition = positions[positionRow.get_selected()];
+            this._settings.set_string('panel-position', newPosition);
+        });
+
+        panelGroup.add(positionRow);
+
+        // Panel index selector
+        const indexRow = new Adw.SpinRow({
+            title: 'Panel Index',
+            subtitle: 'Fine-tune position within the panel (0=leftmost, -1=rightmost)',
+        });
+
+        indexRow.set_adjustment(new Gtk.Adjustment({
+            lower: -1,
+            upper: 99,
+            step_increment: 1,
+            page_increment: 10,
+        }));
+
+        // Set initial value
+        const currentIndex = this._settings.get_int('panel-index');
+        indexRow.set_value(currentIndex);
+
+        // Connect to changes
+        indexRow.connect('changed', () => {
+            this._settings.set_int('panel-index', indexRow.get_value());
+        });
+
+        panelGroup.add(indexRow);
+
+        // Create a preferences group for D-Day list
+        const group = new Adw.PreferencesGroup({
+            title: 'D-Day Events',
+            description: 'Manage your countdown events',
+        });
+        page.add(group);
+
+        // Create ListBox for events
+        const listBox = new Gtk.ListBox({
+            selection_mode: Gtk.SelectionMode.NONE,
+            css_classes: ['boxed-list'],
+        });
+        group.add(listBox);
+
+        // Load and display events
+        this._listBox = listBox;
+        this._radioGroup = null;
+        this._loadEvents();
+
+        // Add button to create new event
+        const addButton = new Gtk.Button({
+            label: 'Add D-Day Event',
+            css_classes: ['suggested-action'],
+            margin_top: 12,
+        });
+        addButton.connect('clicked', () => this._onAddEvent(window));
+        group.add(addButton);
+
+        // Listen for settings changes
+        this._settingsChangedId = this._settings.connect('changed::dday-events',
+            () => this._loadEvents());
+
+        // Create About page
+        this._addAboutPage(window);
+    }
+
+    _loadEvents() {
+        // Clear existing rows
+        let child = this._listBox.get_first_child();
+        while (child) {
+            const next = child.get_next_sibling();
+            this._listBox.remove(child);
+            child = next;
+        }
+
+        // Reset radio group
+        this._radioGroup = null;
+
+        // Load events from settings
+        const eventsJson = this._settings.get_string('dday-events');
+        const selectedId = this._settings.get_string('selected-event-id');
+
+        try {
+            const events = JSON.parse(eventsJson);
+
+            events.forEach(event => {
+                const row = this._createEventRow(event, event.id === selectedId);
+                this._listBox.append(row);
+            });
+
+            // Show placeholder if no events
+            if (events.length === 0) {
+                const row = new Adw.ActionRow({
+                    title: 'No D-Day events yet',
+                    subtitle: 'Click "Add D-Day Event" to create your first countdown',
+                });
+                this._listBox.append(row);
+            }
+
+        } catch (e) {
+            console.error('Doomsday: Failed to load events:', e);
+        }
+    }
+
+    _createEventRow(event, isSelected) {
+        const row = new Adw.ActionRow({
+            title: event.name,
+            subtitle: event.date,
+        });
+
+        // Add radio button for selection
+        const radioButton = new Gtk.CheckButton({
+            active: isSelected,
+            valign: Gtk.Align.CENTER,
+        });
+        radioButton.connect('toggled', () => {
+            if (radioButton.get_active()) {
+                this._settings.set_string('selected-event-id', event.id);
+            }
+        });
+        row.add_prefix(radioButton);
+
+        // Make all radio buttons part of same group
+        if (!this._radioGroup) {
+            this._radioGroup = radioButton;
+        } else {
+            radioButton.set_group(this._radioGroup);
+        }
+
+        // Add edit button
+        const editButton = new Gtk.Button({
+            icon_name: 'document-edit-symbolic',
+            label: 'Edit',
+            valign: Gtk.Align.CENTER,
+            css_classes: ['flat'],
+            tooltip_text: 'Edit this event',
+        });
+        editButton.connect('clicked', () => this._onEditEvent(event));
+        row.add_suffix(editButton);
+
+        // Add delete button
+        const deleteButton = new Gtk.Button({
+            icon_name: 'user-trash-symbolic',
+            label: 'Delete',
+            valign: Gtk.Align.CENTER,
+            css_classes: ['flat', 'destructive-action'],
+            tooltip_text: 'Delete this event',
+        });
+        deleteButton.connect('clicked', () => this._onDeleteEvent(event.id));
+        row.add_suffix(deleteButton);
+
+        return row;
+    }
+
+    _onAddEvent(window) {
+        this._showEventDialog(window, null);
+    }
+
+    _onEditEvent(event) {
+        const window = this._listBox.get_root();
+        this._showEventDialog(window, event);
+    }
+
+    _showEventDialog(window, existingEvent) {
+        const dialog = new Adw.MessageDialog({
+            transient_for: window,
+            modal: true,
+            heading: existingEvent ? 'Edit D-Day Event' : 'New D-Day Event',
+        });
+
+        // Create form content
+        const box = new Gtk.Box({
+            orientation: Gtk.Orientation.VERTICAL,
+            spacing: 12,
+            margin_top: 12,
+            margin_bottom: 12,
+            margin_start: 12,
+            margin_end: 12,
+        });
+
+        // Event name entry
+        const nameEntry = new Adw.EntryRow({
+            title: 'Event Name',
+        });
+        if (existingEvent) {
+            nameEntry.set_text(existingEvent.name);
+        }
+        box.append(nameEntry);
+
+        // Date picker (using entry for simplicity)
+        const dateEntry = new Adw.EntryRow({
+            title: 'Date (YYYY-MM-DD)',
+        });
+        if (existingEvent) {
+            dateEntry.set_text(existingEvent.date);
+        } else {
+            // Default to today
+            const today = new Date();
+            const dateStr = today.toISOString().split('T')[0];
+            dateEntry.set_text(dateStr);
+        }
+        box.append(dateEntry);
+
+        dialog.set_extra_child(box);
+
+        // Add buttons
+        dialog.add_response('cancel', 'Cancel');
+        dialog.add_response('save', 'Save');
+        dialog.set_response_appearance('save', Adw.ResponseAppearance.SUGGESTED);
+
+        dialog.connect('response', (dialog, response) => {
+            if (response === 'save') {
+                const name = nameEntry.get_text().trim();
+                const date = dateEntry.get_text().trim();
+
+                // Validate inputs
+                if (!name || !date) {
+                    console.error('Doomsday: Name and date are required');
+                    return;
+                }
+
+                // Validate date format
+                if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
+                    console.error('Doomsday: Invalid date format');
+                    return;
+                }
+
+                // Save event
+                this._saveEvent(existingEvent?.id, name, date);
+            }
+            dialog.close();
+        });
+
+        dialog.present();
+    }
+
+    _saveEvent(eventId, name, date) {
+        const eventsJson = this._settings.get_string('dday-events');
+        let events = [];
+
+        try {
+            events = JSON.parse(eventsJson);
+        } catch (e) {
+            console.error('Doomsday: Failed to parse events:', e);
+        }
+
+        if (eventId) {
+            // Update existing event
+            const index = events.findIndex(e => e.id === eventId);
+            if (index !== -1) {
+                events[index] = { id: eventId, name, date };
+            }
+        } else {
+            // Create new event with UUID
+            const newId = GLib.uuid_string_random();
+            events.push({ id: newId, name, date });
+
+            // Auto-select if it's the first event
+            if (events.length === 1) {
+                this._settings.set_string('selected-event-id', newId);
+            }
+        }
+
+        // Save back to settings
+        this._settings.set_string('dday-events', JSON.stringify(events));
+    }
+
+    _onDeleteEvent(eventId) {
+        const eventsJson = this._settings.get_string('dday-events');
+        let events = [];
+
+        try {
+            events = JSON.parse(eventsJson);
+        } catch (e) {
+            console.error('Doomsday: Failed to parse events:', e);
+            return;
+        }
+
+        // Remove event
+        events = events.filter(e => e.id !== eventId);
+
+        // If deleted event was selected, clear selection or select first
+        const selectedId = this._settings.get_string('selected-event-id');
+        if (selectedId === eventId) {
+            const newSelectedId = events.length > 0 ? events[0].id : '';
+            this._settings.set_string('selected-event-id', newSelectedId);
+        }
+
+        // Save back to settings
+        this._settings.set_string('dday-events', JSON.stringify(events));
+    }
+
+    _addAboutPage(window) {
+        // Create About page
+        const aboutPage = new Adw.PreferencesPage({
+            title: 'About',
+            icon_name: 'help-about-symbolic',
+        });
+        window.add(aboutPage);
+
+        // Extension info group
+        const infoGroup = new Adw.PreferencesGroup({
+            title: 'Doomsday Extension',
+            description: 'D-Day countdown for GNOME Shell',
+        });
+        aboutPage.add(infoGroup);
+
+        // Version row
+        const versionRow = new Adw.ActionRow({
+            title: 'Version',
+            subtitle: '1.0',
+        });
+        infoGroup.add(versionRow);
+
+        // Links group
+        const linksGroup = new Adw.PreferencesGroup({
+            title: 'Links',
+        });
+        aboutPage.add(linksGroup);
+
+        // GitHub button
+        const githubRow = new Adw.ActionRow({
+            title: 'Doomsday on GitHub',
+            subtitle: 'Visit the project repository',
+            activatable: true,
+        });
+        const githubIcon = new Gtk.Image({
+            icon_name: 'web-browser-symbolic',
+        });
+        githubRow.add_prefix(githubIcon);
+        githubRow.connect('activated', () => {
+            Gtk.show_uri(window, 'https://github.com/devastator-x/doomsday', 0);
+        });
+        linksGroup.add(githubRow);
+
+        // Report Issue button
+        const issueRow = new Adw.ActionRow({
+            title: 'Report an Issue',
+            subtitle: 'Help improve this extension',
+            activatable: true,
+        });
+        const issueIcon = new Gtk.Image({
+            icon_name: 'dialog-warning-symbolic',
+        });
+        issueRow.add_prefix(issueIcon);
+        issueRow.connect('activated', () => {
+            Gtk.show_uri(window, 'https://github.com/devastator-x/doomsday/issues', 0);
+        });
+        linksGroup.add(issueRow);
+
+        // Legal group
+        const legalGroup = new Adw.PreferencesGroup({
+            title: 'Legal',
+        });
+        aboutPage.add(legalGroup);
+
+        // License button
+        const licenseRow = new Adw.ActionRow({
+            title: 'License',
+            subtitle: 'GPL-3.0',
+            activatable: true,
+        });
+        const licenseIcon = new Gtk.Image({
+            icon_name: 'text-x-generic-symbolic',
+        });
+        licenseRow.add_prefix(licenseIcon);
+        licenseRow.connect('activated', () => {
+            this._showLicenseDialog(window);
+        });
+        legalGroup.add(licenseRow);
+
+        // Author row
+        const authorRow = new Adw.ActionRow({
+            title: 'Author',
+            subtitle: 'devastator-x',
+        });
+        const authorIcon = new Gtk.Image({
+            icon_name: 'avatar-default-symbolic',
+        });
+        authorRow.add_prefix(authorIcon);
+        legalGroup.add(authorRow);
+    }
+
+    _showLicenseDialog(window) {
+        const dialog = new Adw.MessageDialog({
+            transient_for: window,
+            modal: true,
+            heading: 'License Information',
+            body: 'This extension is licensed under GPL-3.0.\n\n' +
+                  'This program is free software: you can redistribute it and/or modify ' +
+                  'it under the terms of the GNU General Public License as published by ' +
+                  'the Free Software Foundation, either version 3 of the License, or ' +
+                  '(at your option) any later version.\n\n' +
+                  'This program is distributed in the hope that it will be useful, ' +
+                  'but WITHOUT ANY WARRANTY; without even the implied warranty of ' +
+                  'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ' +
+                  'GNU General Public License for more details.',
+        });
+
+        dialog.add_response('close', 'Close');
+        dialog.set_response_appearance('close', Adw.ResponseAppearance.SUGGESTED);
+        dialog.connect('response', () => dialog.close());
+        dialog.present();
+    }
+}
diff --git a/schemas/org.gnome.shell.extensions.doomsday.gschema.xml b/schemas/org.gnome.shell.extensions.doomsday.gschema.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema id="org.gnome.shell.extensions.doomsday"
+          path="/org/gnome/shell/extensions/doomsday/">
+
+    <!-- Array of D-Day events stored as JSON string -->
+    <key type="s" name="dday-events">
+      <default>'[]'</default>
+      <summary>D-Day Events</summary>
+      <description>
+        JSON array of D-Day events with format:
+        [{"id": "uuid-1", "name": "결혼식", "date": "2026-02-06"}, ...]
+      </description>
+    </key>
+
+    <!-- Currently selected D-Day event ID -->
+    <key type="s" name="selected-event-id">
+      <default>''</default>
+      <summary>Selected Event ID</summary>
+      <description>UUID of the currently displayed D-Day event</description>
+    </key>
+
+    <!-- Position in panel (optional for future enhancement) -->
+    <key type="s" name="panel-position">
+      <default>'center'</default>
+      <summary>Panel Position</summary>
+      <description>Position in top panel: left, center, or right</description>
+    </key>
+
+    <!-- Index within the panel box -->
+    <key type="i" name="panel-index">
+      <default>0</default>
+      <range min="-1" max="99"/>
+      <summary>Panel Index</summary>
+      <description>Position index within the panel box (-1 for rightmost, 0 for leftmost)</description>
+    </key>
+
+  </schema>
+</schemalist>
diff --git a/stylesheet.css b/stylesheet.css
@@ -0,0 +1,47 @@
+.doomsday-label {
+    font-weight: bold;
+    padding: 0 8px;
+}
+
+.panel-button:hover .doomsday-label {
+    color: #fff;
+}
+
+/* Menu item styles */
+.doomsday-menu-item-box {
+    spacing: 8px;
+    padding: 4px 0;
+}
+
+.doomsday-edit-button,
+.doomsday-delete-button {
+    padding: 4px 8px;
+    border-radius: 4px;
+}
+
+/* Dialog styles */
+.doomsday-dialog-title {
+    font-size: 16pt;
+    font-weight: bold;
+    margin-bottom: 16px;
+}
+
+.doomsday-dialog-row {
+    spacing: 8px;
+    margin: 8px 0;
+}
+
+.doomsday-dialog-label {
+    min-width: 60px;
+    font-weight: bold;
+}
+
+.doomsday-dialog-entry {
+    min-width: 200px;
+    padding: 4px 8px;
+}
+
+.doomsday-dialog-message {
+    font-size: 12pt;
+    margin: 16px;
+}