@@ -6,8 +6,20 @@ function initializeInputs() {
66 // set year max to year + 1
77 document . getElementById ( "yearInput" ) . max = year + 1 ;
88 document . getElementById ( "monthInput" ) . value = month ;
9+ // add browser to session storage
10+ const ua = navigator . userAgent ;
11+ const isChrome = ua . includes ( "Chrome" ) && ! ua . includes ( "Edg" ) ;
12+ sessionStorage . setItem ( "isChrome" , isChrome ? "true" : "false" ) ;
13+ if ( isChrome ) {
14+ // show checkbox if auto import is enabled
15+ const autoImportCheckbox = document . getElementById ( "autoImportContainer" ) ;
16+ autoImportCheckbox . classList . remove ( "invisible" ) ;
17+ autoImportCheckbox . querySelector ( "input" ) . checked = true ; // default checked
18+ } else {
19+ // remove checkbox if not chrome
20+ document . getElementById ( "autoImportContainer" ) . remove ( ) ;
21+ }
922}
10-
1123async function fetchCalendarData ( sesskey ) {
1224 const year = parseInt ( document . getElementById ( "yearInput" ) . value ) ;
1325 const month = parseInt ( document . getElementById ( "monthInput" ) . value ) ;
@@ -73,6 +85,159 @@ function setupEventListeners() {
7385 } ) ;
7486 } ) ;
7587 } ) ;
88+
89+ document . getElementById ( "importBtn" ) . addEventListener ( "click" , async ( ) => {
90+ chrome . tabs . query ( { active : true , currentWindow : true } , ( tabs ) => {
91+ chrome . tabs . sendMessage ( tabs [ 0 ] . id , { action : "get_sesskey" } , async ( res ) => {
92+ const sesskey = res ?. sesskey ;
93+ if ( ! sesskey ) {
94+ console . log ( "no touch sesskey" ) ;
95+ document . getElementById ( "result" ) . textContent = "無法獲取 sesskey,請先登入 Moodle" ;
96+ return ;
97+ }
98+
99+ try {
100+ const events = await fetchCalendarData ( sesskey ) ;
101+ insertEventsToGCal ( events ) ;
102+ document . getElementById ( "result" ) . textContent = `成功匯入 ${ events . length } 個行事曆事件` ;
103+ } catch ( error ) {
104+ console . error ( error ) ;
105+ document . getElementById ( "result" ) . textContent = "發生錯誤,請稍後再試" ;
106+ }
107+ } ) ;
108+ } ) ;
109+ } ) ;
110+ }
111+
112+ async function insertEventsToGCal ( events ) {
113+ // Google OAuth token
114+ const token = await getGoogleAuthToken ( ) ;
115+
116+ // add event Google Calendar
117+ for ( const event of events ) {
118+ try {
119+ await addEventToCalendar ( token , event ) ;
120+ console . log ( "add Event Success" , event . title ) ;
121+ } catch ( err ) {
122+ console . error ( "add Event error:" , event . title , err ) ;
123+ }
124+ }
125+
126+ // batch API,done after...
127+ }
128+
129+ async function launchWebAuthFlowForGoogle ( ) {
130+ return new Promise ( ( resolve , reject ) => {
131+ // Except Chrome Client ID
132+ const clientId = import . meta. env . VITE_NOT_CHROME_CLIENT_ID ;
133+ const scope = "https://www.googleapis.com/auth/calendar.events" ;
134+
135+ const redirectUri = `https://${ chrome . runtime . id } .chromiumapp.org/` ;
136+
137+ const authUrl =
138+ "https://accounts.google.com/o/oauth2/v2/auth" +
139+ `?response_type=token` +
140+ `&client_id=${ encodeURIComponent ( clientId ) } ` +
141+ `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
142+ `&scope=${ encodeURIComponent ( scope ) } ` +
143+ `&prompt=consent` ;
144+
145+ chrome . identity . launchWebAuthFlow (
146+ {
147+ url : authUrl ,
148+ interactive : true ,
149+ } ,
150+ ( responseUrl ) => {
151+ if ( chrome . runtime . lastError ) {
152+ console . error ( "launchWebAuthFlow error:" , chrome . runtime . lastError ) ;
153+ return reject ( chrome . runtime . lastError ) ;
154+ }
155+ if ( ! responseUrl ) {
156+ return reject ( "Unable to obtain authorization result" ) ;
157+ }
158+
159+ const urlFragment = responseUrl . split ( "#" ) [ 1 ] ;
160+ if ( ! urlFragment ) {
161+ return reject ( "Unable to retrieve token from callback URL" ) ;
162+ }
163+ const params = new URLSearchParams ( urlFragment ) ;
164+ const token = params . get ( "access_token" ) ;
165+ if ( ! token ) {
166+ return reject ( "Postback URL has no access_token" ) ;
167+ }
168+
169+ resolve ( token ) ;
170+ }
171+ ) ;
172+ } ) ;
173+ }
174+
175+ // get token
176+ function getGoogleAuthToken ( ) {
177+ return new Promise ( ( resolve , reject ) => {
178+ const ua = navigator . userAgent ;
179+ const isChrome = ua . includes ( "Chrome" ) && ! ua . includes ( "Edg" ) ;
180+ console . log ( "ischrome" , isChrome ) ;
181+
182+ // 1. First check if getAuthToken is available (most common in Chrome)
183+ if ( isChrome && chrome . identity && chrome . identity . getAuthToken ) {
184+ console . log ( "Trying chrome.identity.getAuthToken..." ) ;
185+ chrome . identity . getAuthToken ( { interactive : false } , ( token ) => {
186+ if ( chrome . runtime . lastError || ! token ) {
187+ chrome . identity . getAuthToken ( { interactive : true } , ( token2 ) => {
188+ if ( chrome . runtime . lastError || ! token2 ) {
189+ console . error ( "lastError:" , chrome . runtime . lastError ) ;
190+ return reject ( "User denied authorization or an error occurred" ) ;
191+ }
192+ resolve ( token2 ) ;
193+ } ) ;
194+ } else {
195+ resolve ( token ) ;
196+ }
197+ } ) ;
198+
199+ // 2. Otherwise try launchWebAuthFlow (Edge may support it)
200+ } else if ( chrome . identity && chrome . identity . launchWebAuthFlow ) {
201+ console . log ( "Trying chrome.identity.launchWebAuthFlow..." ) ;
202+ launchWebAuthFlowForGoogle ( )
203+ . then ( ( token ) => resolve ( token ) )
204+ . catch ( ( err ) => reject ( err ) ) ;
205+ } else {
206+ reject ( "This browser does not support the chrome.identity API" ) ;
207+ }
208+ } ) ;
209+ }
210+
211+ // Google Calendar API add Event
212+ async function addEventToCalendar ( token , event ) {
213+ // Google Calendar API call
214+ // Docs: https://developers.google.com/calendar/api/v3/reference/events/insert
215+ const res = await fetch ( "https://www.googleapis.com/calendar/v3/calendars/primary/events" , {
216+ method : "POST" ,
217+ headers : {
218+ Authorization : `Bearer ${ token } ` ,
219+ "Content-Type" : "application/json" ,
220+ } ,
221+ body : JSON . stringify ( {
222+ summary : event . title ,
223+ description : event . description ,
224+ start : {
225+ dateTime : event . startDate . toISOString ( ) ,
226+ timeZone : "Asia/Taipei" ,
227+ } ,
228+ end : {
229+ dateTime : event . endDate . toISOString ( ) ,
230+ timeZone : "Asia/Taipei" ,
231+ } ,
232+ } ) ,
233+ } ) ;
234+
235+ if ( ! res . ok ) {
236+ const errText = await res . text ( ) ;
237+ throw new Error ( `API Error: ${ res . status } , ${ errText } ` ) ;
238+ }
239+
240+ return res . json ( ) ;
76241}
77242
78243// Initialize when DOM is fully loaded
0 commit comments