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;
+}
|