11import {
22 Notice ,
33 Plugin ,
4- PluginSettingTab ,
5- Setting ,
6- App ,
7- MarkdownView ,
84 TAbstractFile ,
95 TFile ,
106} from "obsidian" ;
117import { BeeminderApi } from "./beeminder-api" ;
8+ import {
9+ BeeminderSyncSettingTab ,
10+ type BeeminderSyncSettings ,
11+ type SyncedDatapoint ,
12+ DEFAULT_SETTINGS ,
13+ } from "./settings" ;
1214import { BeeminderSuggest } from "./suggest" ;
1315
1416// Regex to match the 🐝 annotation in a task line
@@ -22,30 +24,6 @@ const TASK_LINE_REGEX = /^(\s*)([-*+]|\d+[.)]) \[([^\]])\]\s+(.*)$/u;
2224const TASKS_TRAILING_METADATA_REGEX =
2325 / \s (? = (?: # \S + | (?: 🔺 | ⏫ | 🔼 | 🔽 | ⏬ | 🛫 | ➕ | ⏳ | 📅 | ✅ | ❌ | 🔁 | 🏁 | ⛔ | 🆔 ) \b ) ) / u;
2426
25- interface SyncedDatapoint {
26- goalSlug : string ;
27- datapointId : string ;
28- requestId : string ;
29- }
30-
31- interface BeeminderSyncSettings {
32- username : string ;
33- tokenStorageKey : string ;
34- cachedGoals : { slug : string ; title ?: string } [ ] ;
35- autocompleteMinMatchLength : number ;
36- showNotifications : boolean ;
37- syncedDatapoints : Record < string , SyncedDatapoint > ;
38- }
39-
40- const DEFAULT_SETTINGS : BeeminderSyncSettings = {
41- username : "" ,
42- tokenStorageKey : "beeminder-auth-token" ,
43- cachedGoals : [ ] ,
44- autocompleteMinMatchLength : 1 ,
45- showNotifications : true ,
46- syncedDatapoints : { } ,
47- } ;
48-
4927interface ParsedTask {
5028 lineNumber : number ;
5129 line : string ;
@@ -58,38 +36,6 @@ interface FileSnapshot {
5836 tasks : Map < number , ParsedTask > ;
5937}
6038
61- interface UsageTip {
62- title : string ;
63- body : string ;
64- code ?: string ;
65- }
66-
67- const USAGE_TIPS : UsageTip [ ] = [
68- {
69- title : "Basic" ,
70- body : "Add a 🐝 annotation to any task to send a datapoint when that task is completed." ,
71- code : "- [ ] Read chapter 5 🐝 reading" ,
72- } ,
73- {
74- title : "Custom value" ,
75- body : "Use =number to send a custom value instead of the default +1." ,
76- code : "- [ ] Run 5km 🐝 exercise=5" ,
77- } ,
78- {
79- title : "With Tasks metadata" ,
80- body : "Place 🐝 before trailing Tasks metadata so the annotation is parsed correctly." ,
81- code : "- [ ] Write post 🐝 words=500 📅 2026-04-10" ,
82- } ,
83- {
84- title : "Autocomplete" ,
85- body : 'Type "🐝", "bee", or "goal" on a task line to get suggestions from your cached goals.' ,
86- } ,
87- {
88- title : "Unsync" ,
89- body : "Unchecking a synced task removes the corresponding datapoint from Beeminder." ,
90- } ,
91- ] ;
92-
9339function parseTaskLine ( line : string , lineNumber : number ) : ParsedTask | null {
9440 const match = line . match ( TASK_LINE_REGEX ) ;
9541 if ( ! match ) return null ;
@@ -219,33 +165,11 @@ export default class BeeminderSyncPlugin extends Plugin {
219165 this . fileSnapshots . clear ( ) ;
220166 }
221167
222- // --- Token storage (secret storage with localStorage fallback) ---
223-
224- async storeToken ( token : string ) : Promise < void > {
225- if ( ( this . app as any ) . secretStorage ) {
226- ( this . app as any ) . secretStorage . setSecret ( this . settings . tokenStorageKey , token ) ;
227- return ;
228- }
229- this . app . saveLocalStorage ( this . getLegacyTokenKey ( ) , token ) ;
230- }
231-
232- async clearToken ( ) : Promise < void > {
233- if ( ( this . app as any ) . secretStorage ) {
234- ( this . app as any ) . secretStorage . setSecret ( this . settings . tokenStorageKey , "" ) ;
235- }
236- this . app . saveLocalStorage ( this . getLegacyTokenKey ( ) , null as any ) ;
237- }
168+ // --- Token storage ---
238169
239170 async getToken ( ) : Promise < string | null > {
240- if ( ( this . app as any ) . secretStorage ) {
241- const token = ( this . app as any ) . secretStorage . getSecret ( this . settings . tokenStorageKey ) ;
242- if ( token ) return token ;
243- }
244- return this . app . loadLocalStorage ( this . getLegacyTokenKey ( ) ) ;
245- }
246-
247- private getLegacyTokenKey ( ) : string {
248- return `${ this . manifest . id } :${ this . settings . tokenStorageKey } ` ;
171+ if ( ! this . settings . tokenSecretId ) return null ;
172+ return this . app . secretStorage . getSecret ( this . settings . tokenSecretId ) ;
249173 }
250174
251175 // --- Goal management ---
@@ -484,115 +408,3 @@ export default class BeeminderSyncPlugin extends Plugin {
484408 await this . saveData ( this . settings ) ;
485409 }
486410}
487-
488- class BeeminderSyncSettingTab extends PluginSettingTab {
489- plugin : BeeminderSyncPlugin ;
490-
491- constructor ( app : App , plugin : BeeminderSyncPlugin ) {
492- super ( app , plugin ) ;
493- this . plugin = plugin ;
494- }
495-
496- display ( ) : void {
497- const { containerEl } = this ;
498- containerEl . empty ( ) ;
499-
500- new Setting ( containerEl )
501- . setName ( "Open Beeminder" )
502- . setDesc ( "Get your auth token from Beeminder." )
503- . addButton ( ( btn ) => {
504- btn . setButtonText ( "Open" ) . onClick ( ( ) => {
505- window . open ( "https://www.beeminder.com/api/v1/auth_token.json" , "_blank" , "noopener,noreferrer" ) ;
506- } ) ;
507- } ) ;
508-
509- new Setting ( containerEl )
510- . setName ( "Auth token" )
511- . setDesc ( "Stored in Obsidian secret storage when available." )
512- . addText ( ( text ) => {
513- text . setPlaceholder ( "Paste your Beeminder auth token" ) ;
514- text . onChange ( async ( value ) => {
515- if ( ! value . trim ( ) ) return ;
516- await this . plugin . storeToken ( value . trim ( ) ) ;
517- text . setValue ( "" ) ;
518- new Notice ( "Token saved." ) ;
519- } ) ;
520- } )
521- . addButton ( ( btn ) => {
522- btn . setButtonText ( "Clear" ) . onClick ( async ( ) => {
523- await this . plugin . clearToken ( ) ;
524- new Notice ( "Token cleared." ) ;
525- } ) ;
526- } ) ;
527-
528- new Setting ( containerEl )
529- . setName ( "Username" )
530- . setDesc ( "Auto-filled after token validation." )
531- . addText ( ( text ) =>
532- text
533- . setPlaceholder ( "username" )
534- . setValue ( this . plugin . settings . username )
535- . onChange ( async ( value ) => {
536- this . plugin . settings . username = value . trim ( ) ;
537- await this . plugin . saveSettings ( ) ;
538- } )
539- ) ;
540-
541- new Setting ( containerEl )
542- . setName ( "Validate token & refresh goals" )
543- . setDesc ( "Checks the token and caches your goal list for autocomplete." )
544- . addButton ( ( btn ) => {
545- btn . setButtonText ( "Validate" ) . setCta ( ) . onClick ( async ( ) => {
546- btn . setDisabled ( true ) ;
547- try {
548- await this . plugin . validateAndRefreshGoals ( ) ;
549- new Notice ( `Connected as ${ this . plugin . settings . username } (${ this . plugin . settings . cachedGoals . length } goals)` ) ;
550- this . display ( ) ;
551- } catch ( e ) {
552- new Notice ( e instanceof Error ? e . message : "Validation failed." ) ;
553- } finally {
554- btn . setDisabled ( false ) ;
555- }
556- } ) ;
557- } ) ;
558-
559- new Setting ( containerEl )
560- . setName ( "Autocomplete minimum match length" )
561- . setDesc ( "How many typed characters are required before text-based goal suggestions appear. Set to 0 to show suggestions immediately on task lines." )
562- . addText ( ( text ) =>
563- text
564- . setPlaceholder ( "1" )
565- . setValue ( String ( this . plugin . settings . autocompleteMinMatchLength ) )
566- . onChange ( async ( value ) => {
567- const parsed = Number . parseInt ( value , 10 ) ;
568- this . plugin . settings . autocompleteMinMatchLength =
569- Number . isInteger ( parsed ) && parsed >= 0 ? parsed : DEFAULT_SETTINGS . autocompleteMinMatchLength ;
570- await this . plugin . saveSettings ( ) ;
571- } )
572- ) ;
573-
574- new Setting ( containerEl )
575- . setName ( "Show notifications" )
576- . setDesc ( "Show a notice when datapoints are synced or removed." )
577- . addToggle ( ( toggle ) =>
578- toggle
579- . setValue ( this . plugin . settings . showNotifications )
580- . onChange ( async ( value ) => {
581- this . plugin . settings . showNotifications = value ;
582- await this . plugin . saveSettings ( ) ;
583- } )
584- ) ;
585-
586- containerEl . createEl ( "h3" , { text : "Usage" } ) ;
587- const instructions = containerEl . createDiv ( { cls : "beeminder-settings-help" } ) ;
588-
589- for ( const tip of USAGE_TIPS ) {
590- const tipEl = instructions . createDiv ( { cls : "beeminder-settings-help-tip" } ) ;
591- tipEl . createEl ( "strong" , { text : tip . title } ) ;
592- tipEl . createEl ( "p" , { text : tip . body } ) ;
593- if ( tip . code ) {
594- tipEl . createEl ( "code" , { text : tip . code } ) ;
595- }
596- }
597- }
598- }
0 commit comments