diff --git a/main.js b/main.js index 4ddbba9..9c8b9dd 100644 --- a/main.js +++ b/main.js @@ -3,8 +3,8 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ -var f=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var A=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},I=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!E.call(r,s)&&s!==t&&f(r,s,{get:()=>e[s],enumerable:!(n=v(e,s))||n.enumerable});return r};var U=r=>I(f({},"__esModule",{value:!0}),r);var j={};A(j,{default:()=>k});module.exports=U(j);var y=require("obsidian"),o=require("obsidian");var c=require("obsidian"),w={accessToken:"",clientId:"",clientSecret:"",redirectUri:"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",selectionMode:"line",tagPosition:"append"},u=class extends c.PluginSettingTab{constructor(e,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this;e.empty(),new c.Setting(e).setName("Client id").setDesc("Enter your TickTick Client ID from the Developer Console.").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async n=>{this.plugin.settings.clientId=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client secret").setDesc("Enter your TickTick Client Secret (input is masked).").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async n=>{this.plugin.settings.clientSecret=n.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Enter the Redirect URI registered in your TickTick Developer Portal. (Default: https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app)").addText(t=>t.setPlaceholder("https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").setValue(this.plugin.settings.redirectUri||"").onChange(async n=>{this.plugin.settings.redirectUri=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Selection mode").setDesc("Choose whether the command captures only the current line or the entire paragraph.").addDropdown(t=>t.addOption("line","Current line").addOption("paragraph","Entire paragraph").setValue(this.plugin.settings.selectionMode).onChange(async n=>{this.plugin.settings.selectionMode=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Choose whether the #ticktick tag is added to the beginning or end of the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async n=>{this.plugin.settings.tagPosition=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Access token").setDesc("Your current access token (read-only).").addText(t=>t.setPlaceholder("Access token will appear here").setValue(this.plugin.settings.accessToken||"").onChange(async n=>{this.plugin.settings.accessToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Refresh token").setDesc("Stored refresh token (for debugging purposes).").addText(t=>t.setPlaceholder("Refresh token").setValue(this.plugin.settings.refreshToken||"").onChange(async n=>{this.plugin.settings.refreshToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Connect to TickTick").setDesc("Opens the OAuth authorization page in your browser. After approving, you will be returned to Obsidian automatically.").addButton(async t=>{t.setButtonText("Connect").onClick(async()=>{await this.plugin.startAuthFlow()})})}};function m(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let n=0;n0&&r.getLine(t-1).trim()!=="";)t--;for(;n{if(!e.code){new o.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new o.Notice("TickTick auth failed: state mismatch. Please try connecting again.");return}await this.exchangeAuthCodeForToken(e.code)}),this.addCommand({id:"create-ticktick-task",name:"Create TickTick task",editorCallback:async(e,t)=>{let n=e.getCursor();if(!t.file){new o.Notice("No file found for the current view!");return}let s,a,i;if(this.settings.selectionMode==="paragraph"){let{text:g,start:p,end:P}=N(e,n.line);s=g,a=p,i=P}else{let{text:g,line:p}=D(e,n.line);s=g,a=p,i=p}if(!s.trim()){new o.Notice("No text found on the current line!");return}let d=m(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${s} ^${d}`:`${s} #ticktick ^${d}`;e.replaceRange(l,{line:a,ch:0},{line:i,ch:e.getLine(i).length});let h=this.app.vault.getName(),S=t.file.path,b=`obsidian://advanced-uri?vault=${encodeURIComponent(h)}&filepath=${encodeURIComponent(S)}&block=${encodeURIComponent(d)}`,x=`${s} +var f=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var U=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},I=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!A.call(r,s)&&s!==t&&f(r,s,{get:()=>e[s],enumerable:!(n=E(e,s))||n.enumerable});return r};var D=r=>I(f({},"__esModule",{value:!0}),r);var j={};U(j,{default:()=>k});module.exports=D(j);var S=require("obsidian"),o=require("obsidian");var c=require("obsidian"),y={accessToken:"",clientId:"",clientSecret:"",redirectUri:"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",selectionMode:"line",tagPosition:"append"},u=class extends c.PluginSettingTab{constructor(e,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this;e.empty(),new c.Setting(e).setName("Client id").setDesc("Enter your TickTick Client ID from the Developer Console.").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async n=>{this.plugin.settings.clientId=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client secret").setDesc("Enter your TickTick Client Secret (input is masked).").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async n=>{this.plugin.settings.clientSecret=n.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Enter the Redirect URI registered in your TickTick Developer Portal. (Default: https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app)").addText(t=>t.setPlaceholder("https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").setValue(this.plugin.settings.redirectUri||"").onChange(async n=>{this.plugin.settings.redirectUri=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Selection mode").setDesc("Choose whether the command captures only the current line or the entire paragraph.").addDropdown(t=>t.addOption("line","Current line").addOption("paragraph","Entire paragraph").setValue(this.plugin.settings.selectionMode).onChange(async n=>{this.plugin.settings.selectionMode=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Choose whether the #ticktick tag is added to the beginning or end of the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async n=>{this.plugin.settings.tagPosition=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Access token").setDesc("Your current access token (read-only).").addText(t=>t.setPlaceholder("Access token will appear here").setValue(this.plugin.settings.accessToken||"").onChange(async n=>{this.plugin.settings.accessToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Refresh token").setDesc("Stored refresh token (for debugging purposes).").addText(t=>t.setPlaceholder("Refresh token").setValue(this.plugin.settings.refreshToken||"").onChange(async n=>{this.plugin.settings.refreshToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Connect to TickTick").setDesc("Opens the OAuth authorization page in your browser. After approving, you will be returned to Obsidian automatically.").addButton(async t=>{t.setButtonText("Connect").onClick(async()=>{await this.plugin.startAuthFlow()})})}};function m(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let n=0;n0&&r.getLine(t-1).trim()!=="";)t--;for(;n{if(!e.code){new o.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new o.Notice("TickTick auth failed: state mismatch. Please try connecting again.");return}await this.exchangeAuthCodeForToken(e.code)}),this.addCommand({id:"create-ticktick-task",name:"Create TickTick task",editorCallback:async(e,t)=>{let n=e.getCursor();if(!t.file){new o.Notice("No file found for the current view!");return}let s,a,i;if(this.settings.selectionMode==="paragraph"){let{text:h,start:p,end:P}=R(e,n.line);s=h,a=p,i=P}else{let{text:h,line:p}=N(e,n.line);s=h,a=p,i=p}if(!s.trim()){new o.Notice("No text found on the current line!");return}let d=m(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${s} ^${d}`:`${s} #ticktick ^${d}`;e.replaceRange(l,{line:a,ch:0},{line:i,ch:e.getLine(i).length});let g=this.app.vault.getName(),b=t.file.path,x=`obsidian://advanced-uri?vault=${encodeURIComponent(g)}&filepath=${encodeURIComponent(b)}&block=${encodeURIComponent(d)}`,C=`${s} -[Open in Obsidian](${b})`,C=s.length>50?s.substring(0,50)+"...":s;await this.ensureFreshToken();try{(await this.createTicktickTask(C,x)).success?new o.Notice("TickTick task created successfully!"):new o.Notice("Failed to create TickTick task.")}catch(g){T(this,"task_creation_failed",{error:g.message}),new o.Notice(`Failed to create task: ${g.message} -Check console for details.`)}}})}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new o.Notice("Please enter your Client ID in the settings.");return}let t=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",n=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:s,codeChallenge:a}=await R(),i=m(32);this.settings.tempCodeVerifier=s,this.settings.tempState=i,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(t)}&response_type=code&scope=${n}&code_challenge=${a}&code_challenge_method=S256&state=${i}`;window.open(d,"_blank"),new o.Notice("OAuth flow initiated. Please complete it in your browser.")}async exchangeAuthCodeForToken(e){let t="https://ticktick.com/oauth/token",n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",s=this.settings.clientId,a=this.settings.clientSecret,i=this.settings.tempCodeVerifier;if(!i||!s||!a){new o.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:n,client_id:s,client_secret:a,code_verifier:i});try{let l=await(0,o.requestUrl)({url:t,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:d.toString()});if(l.status===200){let h=l.json;h.refresh_token?this.settings.refreshToken=h.refresh_token:(console.warn("Refresh token not received during token exchange."),new o.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=h.access_token,this.settings.tokenExpiry=Date.now()+h.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new o.Notice("TickTick access token obtained successfully!")}else console.error("Token exchange error (status):",l.status),console.log("response.json:",l.json),console.log("response.text:",l.text),new o.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new o.Notice("Error during token exchange.")}}async refreshAccessToken(){if(!this.settings.refreshToken)throw new Error("No refresh token available. Please reconnect.");let e="https://ticktick.com/oauth/token",t=this.settings.clientId,n=this.settings.clientSecret,s=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:t,client_secret:n});try{let a=await(0,o.requestUrl)({url:e,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:s.toString()});if(a.status===200){let i=a.json;i.refresh_token&&(this.settings.refreshToken=i.refresh_token),this.settings.accessToken=i.access_token,this.settings.tokenExpiry=Date.now()+i.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",a.text),new Error("Failed to refresh token")}catch(a){throw console.error("Error refreshing access token:",a),a}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){T(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new o.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,t){let n=this.settings.accessToken;if(!n)return new o.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let s="https://api.ticktick.com/open/v1/task",a={title:e,content:t};try{T(this,"task_creation_request",{endpoint:s,title:e});let i=await(0,o.requestUrl)({url:s,method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(a)});return i.status===200?{success:!0}:(console.error("TickTick API error (status):",i.status),console.log("response.json:",i.json),console.log("response.text:",i.text),{success:!1})}catch(i){return console.error("TickTick API error:",i),{success:!1}}}async loadSettings(){this.settings=Object.assign({},w,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}}; +[Open in Obsidian](${x})`,v=s.length>50?s.substring(0,50)+"...":s;await this.ensureFreshToken();try{(await this.createTicktickTask(v,C)).success?new o.Notice("TickTick task created successfully!"):new o.Notice("Failed to create TickTick task.")}catch(h){T(this,"task_creation_failed",{error:h.message}),new o.Notice(`Failed to create task: ${h.message} +Check console for details.`)}}})}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new o.Notice("Please enter your Client ID in the settings.");return}let t=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",n=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:s,codeChallenge:a}=await O(),i=m(32);this.settings.tempCodeVerifier=s,this.settings.tempState=i,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(t)}&response_type=code&scope=${n}&code_challenge=${a}&code_challenge_method=S256&state=${i}`;new w(this.app,d).open()}async exchangeAuthCodeForToken(e){let t="https://ticktick.com/oauth/token",n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",s=this.settings.clientId,a=this.settings.clientSecret,i=this.settings.tempCodeVerifier;if(!i||!s||!a){new o.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:n,client_id:s,client_secret:a,code_verifier:i});try{let l=await(0,o.requestUrl)({url:t,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:d.toString()});if(l.status===200){let g=l.json;g.refresh_token?this.settings.refreshToken=g.refresh_token:(console.warn("Refresh token not received during token exchange."),new o.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=g.access_token,this.settings.tokenExpiry=Date.now()+g.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new o.Notice("TickTick access token obtained successfully!")}else console.error("Token exchange error (status):",l.status),console.log("response.json:",l.json),console.log("response.text:",l.text),new o.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new o.Notice("Error during token exchange.")}}async refreshAccessToken(){if(!this.settings.refreshToken)throw new Error("No refresh token available. Please reconnect.");let e="https://ticktick.com/oauth/token",t=this.settings.clientId,n=this.settings.clientSecret,s=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:t,client_secret:n});try{let a=await(0,o.requestUrl)({url:e,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:s.toString()});if(a.status===200){let i=a.json;i.refresh_token&&(this.settings.refreshToken=i.refresh_token),this.settings.accessToken=i.access_token,this.settings.tokenExpiry=Date.now()+i.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",a.text),new Error("Failed to refresh token")}catch(a){throw console.error("Error refreshing access token:",a),a}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){T(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new o.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,t){let n=this.settings.accessToken;if(!n)return new o.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let s="https://api.ticktick.com/open/v1/task",a={title:e,content:t};try{T(this,"task_creation_request",{endpoint:s,title:e});let i=await(0,o.requestUrl)({url:s,method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(a)});return i.status===200?{success:!0}:(console.error("TickTick API error (status):",i.status),console.log("response.json:",i.json),console.log("response.text:",i.text),{success:!1})}catch(i){return console.error("TickTick API error:",i),{success:!1}}}async loadSettings(){this.settings=Object.assign({},y,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}}; diff --git a/main.ts b/main.ts index d080f41..fbc78b6 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,5 @@ import { arrayBufferToBase64 } from 'obsidian'; -import { App, Editor, MarkdownView, Notice, Plugin, requestUrl } from 'obsidian'; +import { App, Editor, MarkdownView, Modal, Notice, Plugin, requestUrl } from 'obsidian'; import { TickTickSettingTab, TickTickSettings, DEFAULT_SETTINGS } from './settings'; // Helper function to generate a random alphanumeric string (for block anchors and state) @@ -62,6 +62,37 @@ async function generatePKCECodes(): Promise<{ codeVerifier: string; codeChalleng return { codeVerifier, codeChallenge }; } +// Modal that presents the OAuth URL as a tappable link. +// iOS blocks programmatic window.open after async work loses the user gesture, +// so we let the user tap the link directly — that tap is itself the gesture. +class AuthLinkModal extends Modal { + constructor(app: App, private authUrl: string) { + super(app); + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl('h2', { text: 'Authorize TickTick' }); + contentEl.createEl('p', { + text: 'Tap the link below to open the TickTick login page. After you approve access, you will be returned to Obsidian automatically.' + }); + const link = contentEl.createEl('a', { + text: 'Open TickTick login', + href: this.authUrl + }); + link.setAttr('target', '_blank'); + link.setAttr('rel', 'noopener'); + link.style.display = 'inline-block'; + link.style.marginTop = '0.5em'; + link.style.fontWeight = 'bold'; + } + + onClose() { + this.contentEl.empty(); + } +} + // Structured logging helper function log(plugin: TickTickPlugin, action: string, metadata?: Record) { console.log(JSON.stringify({ @@ -181,8 +212,7 @@ export default class TickTickPlugin extends Plugin { this.settings.tempState = state; await this.saveSettings(); const authUrl = `https://ticktick.com/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}`; - window.open(authUrl, '_blank'); - new Notice('OAuth flow initiated. Please complete it in your browser.'); + new AuthLinkModal(this.app, authUrl).open(); } /**