floq 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +90 -0
- package/README.md +90 -0
- package/dist/calendar/google-api.d.ts +16 -0
- package/dist/calendar/google-api.js +65 -0
- package/dist/calendar/index.d.ts +40 -0
- package/dist/calendar/index.js +253 -0
- package/dist/calendar/oauth.d.ts +32 -0
- package/dist/calendar/oauth.js +154 -0
- package/dist/cli.js +79 -0
- package/dist/commands/calendar.d.ts +42 -0
- package/dist/commands/calendar.js +302 -0
- package/dist/commands/config.js +17 -1
- package/dist/config.d.ts +32 -0
- package/dist/config.js +61 -0
- package/dist/i18n/en.d.ts +35 -0
- package/dist/i18n/en.js +20 -0
- package/dist/i18n/ja.js +20 -0
- package/dist/ui/App.js +13 -2
- package/dist/ui/components/CalendarEvents.d.ts +9 -0
- package/dist/ui/components/CalendarEvents.js +86 -0
- package/dist/ui/components/CalendarModal.d.ts +6 -0
- package/dist/ui/components/CalendarModal.js +73 -0
- package/dist/ui/components/GtdDQ.js +11 -1
- package/dist/ui/components/GtdMario.js +10 -1
- package/dist/ui/components/HelpModal.js +1 -0
- package/dist/ui/components/KanbanBoard.js +12 -1
- package/dist/ui/components/KanbanDQ.js +12 -1
- package/dist/ui/components/KanbanMario.js +12 -1
- package/package.json +5 -2
package/README.ja.md
CHANGED
|
@@ -16,6 +16,7 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
|
|
|
16
16
|
- **タスク検索**: `/` キーで全タスクを素早く検索
|
|
17
17
|
- **コメント**: タスクにメモやコメントを追加
|
|
18
18
|
- **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
|
|
19
|
+
- **Googleカレンダー**: iCal URLまたはOAuth連携で今日の予定を表示
|
|
19
20
|
- **テーマ**: MS-DOSノスタルジックスタイルやドラクエRPG風を含む複数テーマ
|
|
20
21
|
- **スプラッシュ画面**: 起動時のスプラッシュ画面(レトロテーマではドラクエ風)
|
|
21
22
|
- **多言語対応**: 英語・日本語サポート
|
|
@@ -259,6 +260,95 @@ floq config turso --disable
|
|
|
259
260
|
- TUIヘッダーに接続状態を表示(Tursoはクラウドアイコン、ローカルはローカルアイコン)
|
|
260
261
|
- CLIコマンド実行時、Turso有効時は`🔄 Turso sync: hostname`を表示
|
|
261
262
|
|
|
263
|
+
## Googleカレンダー連携
|
|
264
|
+
|
|
265
|
+
FloqはGoogleカレンダーの予定をTUIに表示できます。2つの方法があります:
|
|
266
|
+
|
|
267
|
+
### 方法1: iCal URL(シンプル、認証不要)
|
|
268
|
+
|
|
269
|
+
GoogleカレンダーのシークレットiCal URLを使用して、OAuth設定なしで読み取り専用アクセスができます。
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# GoogleカレンダーからiCal URLを取得:
|
|
273
|
+
# 設定 > (カレンダー名) > カレンダーの統合 > "iCal形式の非公開URL"
|
|
274
|
+
|
|
275
|
+
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "マイカレンダー"
|
|
276
|
+
floq calendar show
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 方法2: Google OAuth(フルAPIアクセス)
|
|
280
|
+
|
|
281
|
+
OAuthを使用すると、より信頼性が高く、すべてのカレンダーにアクセスできます。
|
|
282
|
+
|
|
283
|
+
#### セットアップ
|
|
284
|
+
|
|
285
|
+
1. [Google Cloud Console](https://console.cloud.google.com/)にアクセス
|
|
286
|
+
2. プロジェクトを作成(または既存のものを選択)
|
|
287
|
+
3. **Google Calendar API を有効化**:
|
|
288
|
+
- **APIとサービス > ライブラリ**に移動
|
|
289
|
+
- 「Google Calendar API」を検索
|
|
290
|
+
- **有効にする**をクリック
|
|
291
|
+
4. **OAuth 同意画面を設定**:
|
|
292
|
+
- **APIとサービス > OAuth 同意画面**に移動
|
|
293
|
+
- **外部**を選択して作成
|
|
294
|
+
- アプリ名と必須フィールドを入力
|
|
295
|
+
- **テストユーザー**に自分のメールアドレスを追加
|
|
296
|
+
5. **OAuth 認証情報を作成**:
|
|
297
|
+
- **APIとサービス > 認証情報**に移動
|
|
298
|
+
- **認証情報を作成 > OAuth クライアント ID**をクリック
|
|
299
|
+
- アプリケーションの種類: **テレビと制限付き入力デバイス**(「デスクトップアプリ」ではない)
|
|
300
|
+
- クライアント ID とクライアントシークレットをコピー
|
|
301
|
+
|
|
302
|
+
#### 設定
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# OAuth認証情報を設定(または環境変数を使用)
|
|
306
|
+
floq calendar config --client-id "your-client-id.apps.googleusercontent.com" --client-secret "your-secret"
|
|
307
|
+
|
|
308
|
+
# または環境変数を使用
|
|
309
|
+
export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
|
310
|
+
export GOOGLE_CLIENT_SECRET="your-secret"
|
|
311
|
+
|
|
312
|
+
# Googleでログイン
|
|
313
|
+
floq calendar login
|
|
314
|
+
# → ブラウザが開いて認証画面へ
|
|
315
|
+
# → 表示されたコードを入力
|
|
316
|
+
|
|
317
|
+
# カレンダーを選択
|
|
318
|
+
floq calendar select
|
|
319
|
+
# → カレンダー一覧が表示される
|
|
320
|
+
# → 番号を入力して選択
|
|
321
|
+
|
|
322
|
+
# 設定と今日の予定を表示
|
|
323
|
+
floq calendar show
|
|
324
|
+
|
|
325
|
+
# カレンダーキャッシュを更新
|
|
326
|
+
floq calendar sync
|
|
327
|
+
|
|
328
|
+
# ログアウト
|
|
329
|
+
floq calendar logout
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### カレンダーコマンド
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
# iCalモード
|
|
336
|
+
floq calendar add <url> [-n name] # iCal URLを追加
|
|
337
|
+
floq calendar remove # カレンダーを削除
|
|
338
|
+
|
|
339
|
+
# OAuthモード
|
|
340
|
+
floq calendar config --client-id <id> --client-secret <secret>
|
|
341
|
+
floq calendar login # Google OAuthログイン
|
|
342
|
+
floq calendar logout # OAuthトークンをクリア
|
|
343
|
+
floq calendar select # カレンダー選択(対話式)
|
|
344
|
+
|
|
345
|
+
# 共通コマンド
|
|
346
|
+
floq calendar show # 設定と今日の予定を表示
|
|
347
|
+
floq calendar sync # キャッシュを更新
|
|
348
|
+
floq calendar enable # 表示を有効化
|
|
349
|
+
floq calendar disable # 表示を無効化
|
|
350
|
+
```
|
|
351
|
+
|
|
262
352
|
## テーマ
|
|
263
353
|
|
|
264
354
|
26種類のテーマが利用可能。`floq config theme` でインタラクティブに選択(j/kで移動)。
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
|
|
|
16
16
|
- **Task Search**: Quick search across all tasks with `/`
|
|
17
17
|
- **Comments**: Add notes and comments to tasks
|
|
18
18
|
- **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
|
|
19
|
+
- **Google Calendar**: Display today's events via iCal URL or OAuth integration
|
|
19
20
|
- **Themes**: Multiple themes including MS-DOS nostalgic styles and Dragon Quest RPG style
|
|
20
21
|
- **Splash Screen**: Configurable startup splash with Dragon Quest style for retro themes
|
|
21
22
|
- **i18n**: English and Japanese support
|
|
@@ -259,6 +260,95 @@ floq config turso --disable
|
|
|
259
260
|
- TUI header shows connection status (cloud icon for Turso, local icon for local mode)
|
|
260
261
|
- CLI commands display `🔄 Turso sync: hostname` when Turso is enabled
|
|
261
262
|
|
|
263
|
+
## Google Calendar Integration
|
|
264
|
+
|
|
265
|
+
Floq can display your Google Calendar events in the TUI. Two methods are available:
|
|
266
|
+
|
|
267
|
+
### Option 1: iCal URL (Simple, No Authentication)
|
|
268
|
+
|
|
269
|
+
Use Google Calendar's secret iCal URL for read-only access without OAuth setup.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Get your iCal URL from Google Calendar:
|
|
273
|
+
# Settings > (Your Calendar) > Integrate calendar > "Secret address in iCal format"
|
|
274
|
+
|
|
275
|
+
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "My Calendar"
|
|
276
|
+
floq calendar show
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Option 2: Google OAuth (Full API Access)
|
|
280
|
+
|
|
281
|
+
Use OAuth for better reliability and access to all your calendars.
|
|
282
|
+
|
|
283
|
+
#### Setup
|
|
284
|
+
|
|
285
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
286
|
+
2. Create a project (or select an existing one)
|
|
287
|
+
3. **Enable Google Calendar API**:
|
|
288
|
+
- Go to **APIs & Services > Library**
|
|
289
|
+
- Search for "Google Calendar API"
|
|
290
|
+
- Click **Enable**
|
|
291
|
+
4. **Configure OAuth consent screen**:
|
|
292
|
+
- Go to **APIs & Services > OAuth consent screen**
|
|
293
|
+
- Select **External** and click Create
|
|
294
|
+
- Fill in app name and required fields
|
|
295
|
+
- Add your email to **Test users**
|
|
296
|
+
5. **Create OAuth credentials**:
|
|
297
|
+
- Go to **APIs & Services > Credentials**
|
|
298
|
+
- Click **Create Credentials > OAuth client ID**
|
|
299
|
+
- Application type: **TV and Limited Input devices** (not "Desktop app")
|
|
300
|
+
- Copy the Client ID and Client Secret
|
|
301
|
+
|
|
302
|
+
#### Configuration
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Set OAuth credentials (or use environment variables)
|
|
306
|
+
floq calendar config --client-id "your-client-id.apps.googleusercontent.com" --client-secret "your-secret"
|
|
307
|
+
|
|
308
|
+
# Or use environment variables
|
|
309
|
+
export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
|
310
|
+
export GOOGLE_CLIENT_SECRET="your-secret"
|
|
311
|
+
|
|
312
|
+
# Login with Google
|
|
313
|
+
floq calendar login
|
|
314
|
+
# → Opens browser for authentication
|
|
315
|
+
# → Enter the displayed code when prompted
|
|
316
|
+
|
|
317
|
+
# Select a calendar
|
|
318
|
+
floq calendar select
|
|
319
|
+
# → Shows list of your calendars
|
|
320
|
+
# → Enter the number to select
|
|
321
|
+
|
|
322
|
+
# View configuration and today's events
|
|
323
|
+
floq calendar show
|
|
324
|
+
|
|
325
|
+
# Refresh calendar cache
|
|
326
|
+
floq calendar sync
|
|
327
|
+
|
|
328
|
+
# Logout
|
|
329
|
+
floq calendar logout
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Calendar Commands
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
# iCal mode
|
|
336
|
+
floq calendar add <url> [-n name] # Add iCal URL
|
|
337
|
+
floq calendar remove # Remove calendar
|
|
338
|
+
|
|
339
|
+
# OAuth mode
|
|
340
|
+
floq calendar config --client-id <id> --client-secret <secret>
|
|
341
|
+
floq calendar login # Google OAuth login
|
|
342
|
+
floq calendar logout # Clear OAuth tokens
|
|
343
|
+
floq calendar select # Select calendar (interactive)
|
|
344
|
+
|
|
345
|
+
# Common commands
|
|
346
|
+
floq calendar show # Show config and today's events
|
|
347
|
+
floq calendar sync # Refresh cache
|
|
348
|
+
floq calendar enable # Enable display
|
|
349
|
+
floq calendar disable # Disable display
|
|
350
|
+
```
|
|
351
|
+
|
|
262
352
|
## Themes
|
|
263
353
|
|
|
264
354
|
26 themes available. Use `floq config theme` for interactive selection (j/k to navigate).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CalendarEvent } from './index.js';
|
|
2
|
+
export interface GoogleCalendar {
|
|
3
|
+
id: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
primary?: boolean;
|
|
6
|
+
backgroundColor?: string;
|
|
7
|
+
foregroundColor?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* List all calendars the user has access to
|
|
11
|
+
*/
|
|
12
|
+
export declare function listCalendars(accessToken: string): Promise<GoogleCalendar[]>;
|
|
13
|
+
/**
|
|
14
|
+
* List events from a specific calendar
|
|
15
|
+
*/
|
|
16
|
+
export declare function listEvents(accessToken: string, calendarId: string, timeMin: Date, timeMax: Date): Promise<CalendarEvent[]>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const GOOGLE_CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
|
|
2
|
+
/**
|
|
3
|
+
* List all calendars the user has access to
|
|
4
|
+
*/
|
|
5
|
+
export async function listCalendars(accessToken) {
|
|
6
|
+
const response = await fetch(`${GOOGLE_CALENDAR_API_BASE}/users/me/calendarList`, {
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${accessToken}`,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
const error = await response.text();
|
|
13
|
+
throw new Error(`Failed to list calendars: ${error}`);
|
|
14
|
+
}
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
return data.items.map(item => ({
|
|
17
|
+
id: item.id,
|
|
18
|
+
summary: item.summary,
|
|
19
|
+
primary: item.primary,
|
|
20
|
+
backgroundColor: item.backgroundColor,
|
|
21
|
+
foregroundColor: item.foregroundColor,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* List events from a specific calendar
|
|
26
|
+
*/
|
|
27
|
+
export async function listEvents(accessToken, calendarId, timeMin, timeMax) {
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
timeMin: timeMin.toISOString(),
|
|
30
|
+
timeMax: timeMax.toISOString(),
|
|
31
|
+
singleEvents: 'true', // Expand recurring events
|
|
32
|
+
orderBy: 'startTime',
|
|
33
|
+
maxResults: '100',
|
|
34
|
+
});
|
|
35
|
+
const encodedCalendarId = encodeURIComponent(calendarId);
|
|
36
|
+
const response = await fetch(`${GOOGLE_CALENDAR_API_BASE}/calendars/${encodedCalendarId}/events?${params}`, {
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: `Bearer ${accessToken}`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const error = await response.text();
|
|
43
|
+
throw new Error(`Failed to list events: ${error}`);
|
|
44
|
+
}
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
return data.items
|
|
47
|
+
.filter(item => item.status !== 'cancelled')
|
|
48
|
+
.map(item => {
|
|
49
|
+
const isAllDay = !item.start.dateTime;
|
|
50
|
+
const start = isAllDay
|
|
51
|
+
? new Date(item.start.date + 'T00:00:00')
|
|
52
|
+
: new Date(item.start.dateTime);
|
|
53
|
+
const end = isAllDay
|
|
54
|
+
? new Date(item.end.date + 'T00:00:00')
|
|
55
|
+
: new Date(item.end.dateTime);
|
|
56
|
+
return {
|
|
57
|
+
id: item.id,
|
|
58
|
+
title: item.summary || 'Untitled',
|
|
59
|
+
start,
|
|
60
|
+
end,
|
|
61
|
+
allDay: isAllDay,
|
|
62
|
+
location: item.location,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface CalendarEvent {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
start: Date;
|
|
5
|
+
end: Date;
|
|
6
|
+
allDay: boolean;
|
|
7
|
+
location?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fetch calendar events from the configured source (iCal URL or OAuth)
|
|
11
|
+
*/
|
|
12
|
+
export declare function fetchCalendarEvents(url?: string): Promise<CalendarEvent[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Get cached events or fetch if cache is stale
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCalendarEvents(): Promise<CalendarEvent[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Get today's events from cache (synchronous)
|
|
19
|
+
*/
|
|
20
|
+
export declare function getTodayEvents(): CalendarEvent[];
|
|
21
|
+
/**
|
|
22
|
+
* Get upcoming events (next event for each hour slot)
|
|
23
|
+
*/
|
|
24
|
+
export declare function getUpcomingEvents(limit?: number): CalendarEvent[];
|
|
25
|
+
/**
|
|
26
|
+
* Clear the events cache
|
|
27
|
+
*/
|
|
28
|
+
export declare function clearCalendarCache(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Format time for display (HH:MM format)
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatEventTime(date: Date): string;
|
|
33
|
+
/**
|
|
34
|
+
* Check if an event is currently happening
|
|
35
|
+
*/
|
|
36
|
+
export declare function isEventOngoing(event: CalendarEvent): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get time until event starts (in minutes)
|
|
39
|
+
*/
|
|
40
|
+
export declare function getMinutesUntilEvent(event: CalendarEvent): number;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import ICAL from 'ical.js';
|
|
2
|
+
import { getCalendarConfig, isCalendarEnabled, getCalendarType, getCalendarOAuthConfig } from '../config.js';
|
|
3
|
+
import { getValidAccessToken } from './oauth.js';
|
|
4
|
+
import { listEvents as listGoogleEvents } from './google-api.js';
|
|
5
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
let eventsCache = null;
|
|
7
|
+
/**
|
|
8
|
+
* Parse iCal data and extract events
|
|
9
|
+
*/
|
|
10
|
+
function parseICalData(icalData) {
|
|
11
|
+
const events = [];
|
|
12
|
+
try {
|
|
13
|
+
const jcalData = ICAL.parse(icalData);
|
|
14
|
+
const vcalendar = new ICAL.Component(jcalData);
|
|
15
|
+
const vevents = vcalendar.getAllSubcomponents('vevent');
|
|
16
|
+
for (const vevent of vevents) {
|
|
17
|
+
const event = new ICAL.Event(vevent);
|
|
18
|
+
// Handle recurring events - get occurrences for today and next 7 days
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
21
|
+
const endOfWeek = new Date(startOfToday);
|
|
22
|
+
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
|
23
|
+
if (event.isRecurring()) {
|
|
24
|
+
try {
|
|
25
|
+
const iter = event.iterator();
|
|
26
|
+
let next = iter.next();
|
|
27
|
+
let count = 0;
|
|
28
|
+
const maxOccurrences = 100; // Safety limit
|
|
29
|
+
while (next && count < maxOccurrences) {
|
|
30
|
+
const occurrenceStart = next.toJSDate();
|
|
31
|
+
const occurrenceEnd = new Date(occurrenceStart.getTime() + event.duration.toSeconds() * 1000);
|
|
32
|
+
// Only include occurrences within our time window
|
|
33
|
+
if (occurrenceStart >= startOfToday && occurrenceStart < endOfWeek) {
|
|
34
|
+
events.push({
|
|
35
|
+
id: `${event.uid}-${occurrenceStart.getTime()}`,
|
|
36
|
+
title: event.summary || 'Untitled',
|
|
37
|
+
start: occurrenceStart,
|
|
38
|
+
end: occurrenceEnd,
|
|
39
|
+
allDay: event.startDate.isDate,
|
|
40
|
+
location: event.location || undefined,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Stop if we're past our window
|
|
44
|
+
if (occurrenceStart >= endOfWeek)
|
|
45
|
+
break;
|
|
46
|
+
next = iter.next();
|
|
47
|
+
count++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// If recurring expansion fails, fall back to single event
|
|
52
|
+
const startDate = event.startDate.toJSDate();
|
|
53
|
+
const endDate = event.endDate?.toJSDate() || startDate;
|
|
54
|
+
events.push({
|
|
55
|
+
id: event.uid,
|
|
56
|
+
title: event.summary || 'Untitled',
|
|
57
|
+
start: startDate,
|
|
58
|
+
end: endDate,
|
|
59
|
+
allDay: event.startDate.isDate,
|
|
60
|
+
location: event.location || undefined,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Non-recurring event
|
|
66
|
+
const startDate = event.startDate.toJSDate();
|
|
67
|
+
const endDate = event.endDate?.toJSDate() || startDate;
|
|
68
|
+
events.push({
|
|
69
|
+
id: event.uid,
|
|
70
|
+
title: event.summary || 'Untitled',
|
|
71
|
+
start: startDate,
|
|
72
|
+
end: endDate,
|
|
73
|
+
allDay: event.startDate.isDate,
|
|
74
|
+
location: event.location || undefined,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// Silently fail on parse errors
|
|
81
|
+
console.error('Failed to parse iCal data:', error);
|
|
82
|
+
}
|
|
83
|
+
return events;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalize iCal URL (convert webcal:// to https://)
|
|
87
|
+
*/
|
|
88
|
+
function normalizeUrl(url) {
|
|
89
|
+
if (url.startsWith('webcal://')) {
|
|
90
|
+
return url.replace('webcal://', 'https://');
|
|
91
|
+
}
|
|
92
|
+
return url;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch calendar events via OAuth (Google Calendar API)
|
|
96
|
+
*/
|
|
97
|
+
async function fetchEventsViaOAuth() {
|
|
98
|
+
const oauthConfig = getCalendarOAuthConfig();
|
|
99
|
+
if (!oauthConfig) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const accessToken = await getValidAccessToken();
|
|
103
|
+
if (!accessToken) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
108
|
+
const endOfWeek = new Date(startOfToday);
|
|
109
|
+
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
|
110
|
+
try {
|
|
111
|
+
return await listGoogleEvents(accessToken, oauthConfig.calendarId, startOfToday, endOfWeek);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error('Failed to fetch Google Calendar events:', error);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fetch calendar events via iCal URL
|
|
120
|
+
*/
|
|
121
|
+
async function fetchEventsViaIcal(url) {
|
|
122
|
+
const normalizedUrl = normalizeUrl(url);
|
|
123
|
+
const response = await fetch(normalizedUrl, {
|
|
124
|
+
headers: {
|
|
125
|
+
'User-Agent': 'Floq GTD CLI',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const icalData = await response.text();
|
|
132
|
+
return parseICalData(icalData);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Fetch calendar events from the configured source (iCal URL or OAuth)
|
|
136
|
+
*/
|
|
137
|
+
export async function fetchCalendarEvents(url) {
|
|
138
|
+
try {
|
|
139
|
+
let events;
|
|
140
|
+
const calendarType = getCalendarType();
|
|
141
|
+
if (url) {
|
|
142
|
+
// Explicit URL provided, use iCal mode
|
|
143
|
+
events = await fetchEventsViaIcal(url);
|
|
144
|
+
}
|
|
145
|
+
else if (calendarType === 'oauth') {
|
|
146
|
+
// OAuth mode
|
|
147
|
+
events = await fetchEventsViaOAuth();
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// iCal mode
|
|
151
|
+
const targetUrl = getCalendarConfig()?.url;
|
|
152
|
+
if (!targetUrl) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
events = await fetchEventsViaIcal(targetUrl);
|
|
156
|
+
}
|
|
157
|
+
// Update cache
|
|
158
|
+
eventsCache = {
|
|
159
|
+
events,
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
};
|
|
162
|
+
return events;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// Silently fail on errors
|
|
166
|
+
console.error('Failed to fetch calendar:', error);
|
|
167
|
+
return eventsCache?.events || [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get cached events or fetch if cache is stale
|
|
172
|
+
*/
|
|
173
|
+
export async function getCalendarEvents() {
|
|
174
|
+
if (!isCalendarEnabled()) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
// Check if cache is valid
|
|
178
|
+
if (eventsCache && Date.now() - eventsCache.timestamp < CACHE_TTL_MS) {
|
|
179
|
+
return eventsCache.events;
|
|
180
|
+
}
|
|
181
|
+
// Fetch fresh data
|
|
182
|
+
return fetchCalendarEvents();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get today's events from cache (synchronous)
|
|
186
|
+
*/
|
|
187
|
+
export function getTodayEvents() {
|
|
188
|
+
if (!eventsCache) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
193
|
+
const endOfToday = new Date(startOfToday);
|
|
194
|
+
endOfToday.setDate(endOfToday.getDate() + 1);
|
|
195
|
+
return eventsCache.events
|
|
196
|
+
.filter(event => {
|
|
197
|
+
// Event starts today or spans today
|
|
198
|
+
return (event.start >= startOfToday && event.start < endOfToday) ||
|
|
199
|
+
(event.start < startOfToday && event.end >= startOfToday);
|
|
200
|
+
})
|
|
201
|
+
.sort((a, b) => {
|
|
202
|
+
// All-day events first, then by start time
|
|
203
|
+
if (a.allDay && !b.allDay)
|
|
204
|
+
return -1;
|
|
205
|
+
if (!a.allDay && b.allDay)
|
|
206
|
+
return 1;
|
|
207
|
+
return a.start.getTime() - b.start.getTime();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get upcoming events (next event for each hour slot)
|
|
212
|
+
*/
|
|
213
|
+
export function getUpcomingEvents(limit = 5) {
|
|
214
|
+
if (!eventsCache) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const now = new Date();
|
|
218
|
+
return eventsCache.events
|
|
219
|
+
.filter(event => {
|
|
220
|
+
// Future events or ongoing events
|
|
221
|
+
return event.end > now;
|
|
222
|
+
})
|
|
223
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
|
224
|
+
.slice(0, limit);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Clear the events cache
|
|
228
|
+
*/
|
|
229
|
+
export function clearCalendarCache() {
|
|
230
|
+
eventsCache = null;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Format time for display (HH:MM format)
|
|
234
|
+
*/
|
|
235
|
+
export function formatEventTime(date) {
|
|
236
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
237
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
238
|
+
return `${hours}:${minutes}`;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Check if an event is currently happening
|
|
242
|
+
*/
|
|
243
|
+
export function isEventOngoing(event) {
|
|
244
|
+
const now = new Date();
|
|
245
|
+
return event.start <= now && event.end > now;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get time until event starts (in minutes)
|
|
249
|
+
*/
|
|
250
|
+
export function getMinutesUntilEvent(event) {
|
|
251
|
+
const now = new Date();
|
|
252
|
+
return Math.floor((event.start.getTime() - now.getTime()) / (1000 * 60));
|
|
253
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CalendarOAuthTokens } from '../config.js';
|
|
2
|
+
export interface DeviceCodeResponse {
|
|
3
|
+
deviceCode: string;
|
|
4
|
+
userCode: string;
|
|
5
|
+
verificationUrl: string;
|
|
6
|
+
expiresIn: number;
|
|
7
|
+
interval: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Start the OAuth Device Code flow
|
|
11
|
+
*/
|
|
12
|
+
export declare function startOAuthFlow(): Promise<DeviceCodeResponse>;
|
|
13
|
+
/**
|
|
14
|
+
* Poll for tokens after user has authorized
|
|
15
|
+
*/
|
|
16
|
+
export declare function pollForTokens(deviceCode: string, interval?: number): Promise<CalendarOAuthTokens>;
|
|
17
|
+
/**
|
|
18
|
+
* Refresh the access token using the refresh token
|
|
19
|
+
*/
|
|
20
|
+
export declare function refreshAccessToken(refreshToken: string): Promise<CalendarOAuthTokens>;
|
|
21
|
+
/**
|
|
22
|
+
* Check if the token is expired (with 5-minute buffer)
|
|
23
|
+
*/
|
|
24
|
+
export declare function isTokenExpired(tokens: CalendarOAuthTokens): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Get a valid access token, refreshing if necessary
|
|
27
|
+
*/
|
|
28
|
+
export declare function getValidAccessToken(): Promise<string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Clear OAuth tokens (logout)
|
|
31
|
+
*/
|
|
32
|
+
export declare function clearOAuthTokens(): void;
|