diff --git a/README.md b/README.md index 46cc1a5..7491fe1 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,9 @@ tick habit log "Read 30 min" ```bash tick focus ls +tick focus ls --type 0 # pomodoro (default: 1=timer) tick focus get -tick focus start "Deep work" --project Work -tick focus stop +tick focus get --type 0 # pomodoro (default: 1=timer) ``` ### Configuration @@ -165,7 +165,7 @@ tick config list tick config get region tick config set region ticktick # or dida365 tick config set output json # default output format -tick config set default_project Work # default for quick add / task add / focus start +tick config set default_project Work # default for quick add / task add ``` Switching regions after login requires re-authentication: diff --git a/README.zh-CN.md b/README.zh-CN.md index 9db47e7..9c763ff 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -153,9 +153,9 @@ tick habit log "Read 30 min" ```bash tick focus ls +tick focus ls --type 0 # 番茄钟(默认:1=正计时) tick focus get -tick focus start "Deep work" --project Work -tick focus stop +tick focus get --type 0 # 番茄钟(默认:1=正计时) ``` ### 配置 @@ -165,7 +165,7 @@ tick config list tick config get region tick config set region ticktick # 或 dida365 tick config set output json # 默认输出格式 -tick config set default_project Work # quick add / task add / focus start 的默认项目 +tick config set default_project Work # quick add / task add 的默认项目 ``` 切换区域后需要重新登录: diff --git a/internal/app/focus.go b/internal/app/focus.go index fa955e7..65e8d8c 100644 --- a/internal/app/focus.go +++ b/internal/app/focus.go @@ -9,10 +9,8 @@ import ( ) type FocusAPI interface { - GetFocus(context.Context, string, string) (domain.Focus, error) - ListFocus(context.Context, string, time.Time, time.Time) ([]domain.Focus, error) - StartFocus(context.Context, string, domain.StartFocusInput) (domain.Focus, error) - StopFocus(context.Context, string, string) error + GetFocus(context.Context, string, string, int) (domain.Focus, error) + ListFocus(context.Context, string, time.Time, time.Time, int) ([]domain.Focus, error) ListProjects(context.Context, string) ([]domain.Project, error) } @@ -23,18 +21,9 @@ type FocusApp struct { } type ListFocusInput struct { - From string - To string - Project string -} - -type StartFocusAppInput struct { - Title string - Content string - ProjectRef string - TaskID string - Mode domain.FocusMode - StartRaw string + From string + To string + Type int } func (a FocusApp) List(ctx context.Context, in ListFocusInput) ([]domain.Focus, map[string]string, error) { @@ -64,7 +53,7 @@ func (a FocusApp) List(ctx context.Context, in ListFocusInput) ([]domain.Focus, } } - focuses, err := a.Client.ListFocus(ctx, token, startDate, endDate) + focuses, err := a.Client.ListFocus(ctx, token, startDate, endDate, in.Type) if err != nil { return nil, nil, err } @@ -79,73 +68,15 @@ func (a FocusApp) List(ctx context.Context, in ListFocusInput) ([]domain.Focus, projectNames[p.ID] = p.Name } - if in.Project != "" { - project, err := ResolveProject(in.Project, projects) - if err != nil { - return nil, nil, err - } - filtered := make([]domain.Focus, 0, len(focuses)) - for _, f := range focuses { - if f.ProjectID == project.ID { - filtered = append(filtered, f) - } - } - focuses = filtered - } - return focuses, projectNames, nil } -func (a FocusApp) Get(ctx context.Context, focusID string) (domain.Focus, error) { - token, err := a.Auth.AccessToken(ctx) - if err != nil { - return domain.Focus{}, err - } - return a.Client.GetFocus(ctx, token, focusID) -} - -func (a FocusApp) Start(ctx context.Context, in StartFocusAppInput) (domain.Focus, error) { +func (a FocusApp) Get(ctx context.Context, focusID string, focusType int) (domain.Focus, error) { token, err := a.Auth.AccessToken(ctx) if err != nil { return domain.Focus{}, err } - - projects, err := a.Client.ListProjects(ctx, token) - if err != nil { - return domain.Focus{}, err - } - - project, err := ResolveProject(in.ProjectRef, projects) - if err != nil { - return domain.Focus{}, err - } - - payload := domain.StartFocusInput{ - Title: in.Title, - Content: in.Content, - ProjectID: project.ID, - TaskID: in.TaskID, - Mode: in.Mode, - } - - if in.StartRaw != "" { - loc := time.Local - start, err := domain.ParseUserTime(in.StartRaw, loc) - if err != nil { - return domain.Focus{}, err - } - payload.StartDate = &start - } - - return a.Client.StartFocus(ctx, token, payload) -} - -func (a FocusApp) Stop(ctx context.Context, focusID string) error { - token, err := a.Auth.AccessToken(ctx) - if err != nil { - return err - } - return a.Client.StopFocus(ctx, token, focusID) + return a.Client.GetFocus(ctx, token, focusID, focusType) } func (a FocusApp) ListProjects(ctx context.Context) ([]domain.Project, error) { diff --git a/internal/app/focus_test.go b/internal/app/focus_test.go index c7afe7e..a2a25e2 100644 --- a/internal/app/focus_test.go +++ b/internal/app/focus_test.go @@ -13,8 +13,8 @@ type recordingFocusAPI struct { focuses []domain.Focus lastListStart time.Time lastListEnd time.Time - startCalls []domain.StartFocusInput - stopCalls []string + lastListType int + lastGetType int } func (r *recordingFocusAPI) ListProjects(context.Context, string) ([]domain.Project, error) { @@ -24,7 +24,8 @@ func (r *recordingFocusAPI) ListProjects(context.Context, string) ([]domain.Proj return []domain.Project{{ID: "p1", Name: "Inbox"}}, nil } -func (r *recordingFocusAPI) GetFocus(_ context.Context, _ string, focusID string) (domain.Focus, error) { +func (r *recordingFocusAPI) GetFocus(_ context.Context, _ string, focusID string, focusType int) (domain.Focus, error) { + r.lastGetType = focusType for _, f := range r.focuses { if f.ID == focusID { return f, nil @@ -33,22 +34,13 @@ func (r *recordingFocusAPI) GetFocus(_ context.Context, _ string, focusID string return domain.Focus{}, nil } -func (r *recordingFocusAPI) ListFocus(_ context.Context, _ string, startDate, endDate time.Time) ([]domain.Focus, error) { +func (r *recordingFocusAPI) ListFocus(_ context.Context, _ string, startDate, endDate time.Time, focusType int) ([]domain.Focus, error) { r.lastListStart = startDate r.lastListEnd = endDate + r.lastListType = focusType return r.focuses, nil } -func (r *recordingFocusAPI) StartFocus(_ context.Context, _ string, in domain.StartFocusInput) (domain.Focus, error) { - r.startCalls = append(r.startCalls, in) - return domain.Focus{ID: "f1", Title: in.Title, Mode: in.Mode, ProjectID: in.ProjectID}, nil -} - -func (r *recordingFocusAPI) StopFocus(_ context.Context, _ string, focusID string) error { - r.stopCalls = append(r.stopCalls, focusID) - return nil -} - func TestFocusAppListDefaultTimeRange(t *testing.T) { now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.Local) client := &recordingFocusAPI{focuses: []domain.Focus{{ID: "f1", Title: "Deep work", ProjectID: "p1"}}} @@ -60,7 +52,7 @@ func TestFocusAppListDefaultTimeRange(t *testing.T) { }, } - focuses, _, err := focusApp.List(context.Background(), ListFocusInput{}) + focuses, _, err := focusApp.List(context.Background(), ListFocusInput{Type: 1}) if err != nil { t.Fatalf("List() error = %v", err) } @@ -75,6 +67,9 @@ func TestFocusAppListDefaultTimeRange(t *testing.T) { if !client.lastListEnd.Equal(now) { t.Fatalf("list end = %v, want %v", client.lastListEnd, now) } + if client.lastListType != 1 { + t.Fatalf("list type = %d, want 1", client.lastListType) + } } func TestFocusAppListCustomTimeRange(t *testing.T) { @@ -91,6 +86,7 @@ func TestFocusAppListCustomTimeRange(t *testing.T) { _, _, err := focusApp.List(context.Background(), ListFocusInput{ From: "2026-05-01", To: "2026-05-05", + Type: 0, }) if err != nil { t.Fatalf("List() error = %v", err) @@ -104,62 +100,8 @@ func TestFocusAppListCustomTimeRange(t *testing.T) { if !client.lastListEnd.Equal(wantEnd) { t.Fatalf("list end = %v, want %v", client.lastListEnd, wantEnd) } -} - -func TestFocusAppListFiltersByProject(t *testing.T) { - now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.Local) - client := &recordingFocusAPI{ - projects: []domain.Project{{ID: "p1", Name: "Inbox"}, {ID: "p2", Name: "Work"}}, - focuses: []domain.Focus{ - {ID: "f1", Title: "Personal", ProjectID: "p1"}, - {ID: "f2", Title: "Work", ProjectID: "p2"}, - }, - } - focusApp := FocusApp{ - Auth: stubTokenSource{}, - Client: client, - Now: func() time.Time { - return now - }, - } - - focuses, _, err := focusApp.List(context.Background(), ListFocusInput{Project: "Work"}) - if err != nil { - t.Fatalf("List() error = %v", err) - } - if len(focuses) != 1 { - t.Fatalf("len(focuses) = %d, want 1", len(focuses)) - } - if focuses[0].ID != "f2" { - t.Fatalf("focuses[0].ID = %q, want f2", focuses[0].ID) - } -} - -func TestFocusAppStartResolvesProject(t *testing.T) { - client := &recordingFocusAPI{ - projects: []domain.Project{{ID: "p2", Name: "Work"}}, - } - focusApp := FocusApp{ - Auth: stubTokenSource{}, - Client: client, - } - - focus, err := focusApp.Start(context.Background(), StartFocusAppInput{ - Title: "Deep work", - ProjectRef: "Work", - Mode: domain.FocusModeTimer, - }) - if err != nil { - t.Fatalf("Start() error = %v", err) - } - if focus.ProjectID != "p2" { - t.Fatalf("focus.ProjectID = %q, want p2", focus.ProjectID) - } - if len(client.startCalls) != 1 { - t.Fatalf("start calls = %d, want 1", len(client.startCalls)) - } - if client.startCalls[0].ProjectID != "p2" { - t.Fatalf("start call ProjectID = %q, want p2", client.startCalls[0].ProjectID) + if client.lastListType != 0 { + t.Fatalf("list type = %d, want 0", client.lastListType) } } @@ -172,7 +114,7 @@ func TestFocusAppGet(t *testing.T) { Client: client, } - focus, err := focusApp.Get(context.Background(), "f1") + focus, err := focusApp.Get(context.Background(), "f1", 0) if err != nil { t.Fatalf("Get() error = %v", err) } @@ -182,49 +124,7 @@ func TestFocusAppGet(t *testing.T) { if focus.Title != "Deep work" { t.Fatalf("focus.Title = %q, want Deep work", focus.Title) } -} - -func TestFocusAppStartWithCustomTime(t *testing.T) { - client := &recordingFocusAPI{ - projects: []domain.Project{{ID: "p2", Name: "Work"}}, - } - focusApp := FocusApp{ - Auth: stubTokenSource{}, - Client: client, - } - - _, err := focusApp.Start(context.Background(), StartFocusAppInput{ - Title: "Deep work", - ProjectRef: "Work", - Mode: domain.FocusModeTimer, - StartRaw: "2026-05-01", - }) - if err != nil { - t.Fatalf("Start() error = %v", err) - } - if len(client.startCalls) != 1 { - t.Fatalf("start calls = %d, want 1", len(client.startCalls)) - } - if client.startCalls[0].StartDate == nil { - t.Fatalf("start call StartDate = nil, want non-nil") - } - want := time.Date(2026, 5, 1, 0, 0, 0, 0, time.Local) - if !client.startCalls[0].StartDate.Equal(want) { - t.Fatalf("start call StartDate = %v, want %v", client.startCalls[0].StartDate, want) - } -} - -func TestFocusAppStop(t *testing.T) { - client := &recordingFocusAPI{} - focusApp := FocusApp{ - Auth: stubTokenSource{}, - Client: client, - } - - if err := focusApp.Stop(context.Background(), "f1"); err != nil { - t.Fatalf("Stop() error = %v", err) - } - if len(client.stopCalls) != 1 || client.stopCalls[0] != "f1" { - t.Fatalf("stop calls = %v, want [f1]", client.stopCalls) + if client.lastGetType != 0 { + t.Fatalf("get type = %d, want 0", client.lastGetType) } } diff --git a/internal/cli/focus.go b/internal/cli/focus.go index ee20496..172702e 100644 --- a/internal/cli/focus.go +++ b/internal/cli/focus.go @@ -2,7 +2,6 @@ package cli import ( "errors" - "fmt" "github.com/jeely/ticktick-cli/internal/app" "github.com/jeely/ticktick-cli/internal/domain" @@ -27,7 +26,7 @@ func NewFocusCommand(resolveFocusApp FocusResolver, resolveConfigApp ConfigResol var from string var to string - var project string + var focusType int var jsonOut bool var outputFormat string ls := &cobra.Command{ @@ -39,9 +38,9 @@ func NewFocusCommand(resolveFocusApp FocusResolver, resolveConfigApp ConfigResol return err } input := app.ListFocusInput{ - From: from, - To: to, - Project: project, + From: from, + To: to, + Type: focusType, } focuses, names, err := focusApp.List(cmd.Context(), input) if err != nil { @@ -59,11 +58,12 @@ func NewFocusCommand(resolveFocusApp FocusResolver, resolveConfigApp ConfigResol } ls.Flags().StringVar(&from, "from", "", "Start date (YYYY-MM-DD or RFC3339)") ls.Flags().StringVar(&to, "to", "", "End date (YYYY-MM-DD or RFC3339)") - ls.Flags().StringVar(&project, "project", "", "Project ID or exact name") + ls.Flags().IntVar(&focusType, "type", 1, "Focus type: 0=pomodoro, 1=timer") ls.Flags().StringVar(&outputFormat, "output", "table", "Output format: table or json") ls.Flags().BoolVar(&jsonOut, "json", false, "Print JSON") var getJSON bool + var getType int get := &cobra.Command{ Use: "get ", Short: "Show one focus session by ID", @@ -73,7 +73,7 @@ func NewFocusCommand(resolveFocusApp FocusResolver, resolveConfigApp ConfigResol if err != nil { return err } - focus, err := focusApp.Get(cmd.Context(), args[0]) + focus, err := focusApp.Get(cmd.Context(), args[0], getType) if err != nil { return err } @@ -88,98 +88,9 @@ func NewFocusCommand(resolveFocusApp FocusResolver, resolveConfigApp ConfigResol return output.PrintFocusTable(streams.Out, []domain.Focus{focus}, names) }, } + get.Flags().IntVar(&getType, "type", 1, "Focus type: 0=pomodoro, 1=timer") get.Flags().BoolVar(&getJSON, "json", false, "Print JSON") - var startContent string - var startProject string - var startMode int - var startTaskID string - var startTime string - var startJSON bool - start := &cobra.Command{ - Use: "start ", - Short: "Start a focus session", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - focusApp, err := resolve() - if err != nil { - return err - } - - input := app.StartFocusAppInput{ - Title: args[0], - Content: startContent, - Mode: domain.FocusMode(startMode), - TaskID: startTaskID, - StartRaw: startTime, - } - - if startProject == "" && resolveConfigApp != nil { - configApp, err := resolveConfigApp() - if err != nil { - return err - } - defaultProject, err := configApp.Get(cmd.Context(), "default_project") - if err != nil { - return err - } - startProject = defaultProject - } - - if startProject == "" { - if !IsTerminal(streams) { - return errors.New("no project specified: use --project or set default_project") - } - projects, err := focusApp.ListProjects(cmd.Context()) - if err != nil { - return err - } - if len(projects) == 0 { - return errors.New("no projects available") - } - project, err := SelectProject(streams, projects) - if err != nil { - return err - } - startProject = project.Name - } - - input.ProjectRef = startProject - focus, err := focusApp.Start(cmd.Context(), input) - if err != nil { - return err - } - if startJSON { - return output.PrintJSON(streams.Out, focus) - } - _, err = fmt.Fprintf(streams.Out, "Started focus: %s (%s)\n", focus.ID, focus.Title) - return err - }, - } - start.Flags().StringVar(&startContent, "content", "", "Focus session description") - start.Flags().StringVar(&startProject, "project", "", "Project ID or exact name") - start.Flags().IntVar(&startMode, "mode", 1, "Focus mode: 1=timer, 2=pomodoro") - start.Flags().StringVar(&startTaskID, "task", "", "Associated task ID") - start.Flags().StringVar(&startTime, "start", "", "Start time (YYYY-MM-DD or RFC3339)") - start.Flags().BoolVar(&startJSON, "json", false, "Print JSON") - - stop := &cobra.Command{ - Use: "stop <focus-id>", - Short: "Stop a focus session", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - focusApp, err := resolve() - if err != nil { - return err - } - if err := focusApp.Stop(cmd.Context(), args[0]); err != nil { - return err - } - _, err = fmt.Fprintln(streams.Out, "Stopped") - return err - }, - } - - cmd.AddCommand(ls, get, start, stop) + cmd.AddCommand(ls, get) return cmd } diff --git a/internal/cli/focus_test.go b/internal/cli/focus_test.go index be27cd5..9a32472 100644 --- a/internal/cli/focus_test.go +++ b/internal/cli/focus_test.go @@ -19,7 +19,7 @@ func (f fakeFocusAPI) ListProjects(context.Context, string) ([]domain.Project, e return f.projects, nil } -func (f fakeFocusAPI) GetFocus(_ context.Context, _ string, focusID string) (domain.Focus, error) { +func (f fakeFocusAPI) GetFocus(_ context.Context, _ string, focusID string, focusType int) (domain.Focus, error) { for _, f := range f.focuses { if f.ID == focusID { return f, nil @@ -28,18 +28,10 @@ func (f fakeFocusAPI) GetFocus(_ context.Context, _ string, focusID string) (dom return domain.Focus{}, nil } -func (f fakeFocusAPI) ListFocus(context.Context, string, time.Time, time.Time) ([]domain.Focus, error) { +func (f fakeFocusAPI) ListFocus(context.Context, string, time.Time, time.Time, int) ([]domain.Focus, error) { return f.focuses, nil } -func (f fakeFocusAPI) StartFocus(context.Context, string, domain.StartFocusInput) (domain.Focus, error) { - return domain.Focus{ID: "f1", Title: "Test"}, nil -} - -func (f fakeFocusAPI) StopFocus(context.Context, string, string) error { - return nil -} - func TestFocusListPrintsTable(t *testing.T) { streams, stdout, stderr := newTestStreams() project := domain.Project{ID: "p1", Name: "Inbox"} @@ -128,48 +120,6 @@ func TestFocusGetPrintsTable(t *testing.T) { } } -func TestFocusStartRequiresProject(t *testing.T) { - streams, _, _ := newTestStreams() - cmd := NewFocusCommand(func() (*app.FocusApp, error) { - return &app.FocusApp{}, nil - }, nil, streams) - cmd.SetArgs([]string{"start", "Deep work"}) - - err := cmd.Execute() - if err == nil { - t.Fatal("Execute() error = nil, want non-nil") - } - if !strings.Contains(err.Error(), "no project specified") { - t.Fatalf("error = %q, want no project", err.Error()) - } -} - -func TestFocusStopPrintsStopped(t *testing.T) { - streams, stdout, stderr := newTestStreams() - focusApp := &app.FocusApp{ - Auth: fakeTokenSource{}, - Client: fakeFocusAPI{}, - } - cmd := NewRootCommand(RootOptions{ - Version: "dev", - Streams: streams, - FocusResolver: func() (*app.FocusApp, error) { - return focusApp, nil - }, - }) - cmd.SetArgs([]string{"focus", "stop", "f1"}) - - if err := cmd.Execute(); err != nil { - t.Fatalf("Execute() error = %v", err) - } - if !strings.Contains(stdout.String(), "Stopped") { - t.Fatalf("stdout = %q, want Stopped", stdout.String()) - } - if stderr.Len() != 0 { - t.Fatalf("stderr = %q, want empty", stderr.String()) - } -} - func TestFocusCommandHelpDoesNotResolve(t *testing.T) { streams, stdout, stderr := newTestStreams() resolved := 0 diff --git a/internal/domain/focus.go b/internal/domain/focus.go index 80c7936..4377fc6 100644 --- a/internal/domain/focus.go +++ b/internal/domain/focus.go @@ -20,6 +20,28 @@ func (m FocusMode) String() string { } } +func FocusModeFromAPIType(t int) FocusMode { + switch t { + case 0: + return FocusModePomodoro + case 1: + return FocusModeTimer + default: + return FocusModeTimer + } +} + +func (m FocusMode) APIType() int { + switch m { + case FocusModePomodoro: + return 0 + case FocusModeTimer: + return 1 + default: + return 1 + } +} + type FocusStatus int const ( @@ -55,11 +77,3 @@ type Focus struct { SortOrder int `json:"sortOrder"` } -type StartFocusInput struct { - Title string - Content string - ProjectID string - TaskID string - Mode FocusMode - StartDate *time.Time -} diff --git a/internal/domain/focus_test.go b/internal/domain/focus_test.go index 5e64506..dcb22f7 100644 --- a/internal/domain/focus_test.go +++ b/internal/domain/focus_test.go @@ -33,3 +33,35 @@ func TestFocusStatusString(t *testing.T) { } } } + +func TestFocusModeAPIType(t *testing.T) { + cases := []struct { + mode FocusMode + want int + }{ + {FocusModeTimer, 1}, + {FocusModePomodoro, 0}, + {FocusMode(99), 1}, + } + for _, tc := range cases { + if got := tc.mode.APIType(); got != tc.want { + t.Fatalf("FocusMode(%d).APIType() = %d, want %d", tc.mode, got, tc.want) + } + } +} + +func TestFocusModeFromAPIType(t *testing.T) { + cases := []struct { + apiType int + want FocusMode + }{ + {0, FocusModePomodoro}, + {1, FocusModeTimer}, + {99, FocusModeTimer}, + } + for _, tc := range cases { + if got := FocusModeFromAPIType(tc.apiType); got != tc.want { + t.Fatalf("FocusModeFromAPIType(%d) = %v, want %v", tc.apiType, got, tc.want) + } + } +} diff --git a/internal/ticktick/focus.go b/internal/ticktick/focus.go index fc61fd5..c1919e7 100644 --- a/internal/ticktick/focus.go +++ b/internal/ticktick/focus.go @@ -11,19 +11,14 @@ import ( type focusDTO struct { ID string `json:"id"` - Mode int `json:"mode"` + Type int `json:"type"` Status int `json:"status"` - Title string `json:"title"` - Content string `json:"content"` - ProjectID string `json:"projectId"` + Note string `json:"note"` TaskID string `json:"taskId"` - StartDate string `json:"startDate"` - EndDate string `json:"endDate"` - TimeZone string `json:"timezone"` - AbandonReason string `json:"abandonReason"` - Tags []string `json:"tags"` - Creators []string `json:"creators"` - SortOrder int `json:"sortOrder"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Duration int64 `json:"duration"` + PauseDuration int `json:"pauseDuration"` } type focusListResponse struct { @@ -32,35 +27,31 @@ type focusListResponse struct { func mapFocus(dto focusDTO) domain.Focus { return domain.Focus{ - ID: dto.ID, - Mode: domain.FocusMode(dto.Mode), - Status: domain.FocusStatus(dto.Status), - Title: dto.Title, - Content: dto.Content, - ProjectID: dto.ProjectID, - TaskID: dto.TaskID, - StartDate: parseTickTime(dto.StartDate), - EndDate: parseTickTime(dto.EndDate), - TimeZone: dto.TimeZone, - AbandonReason: dto.AbandonReason, - Tags: dto.Tags, - Creators: dto.Creators, - SortOrder: dto.SortOrder, + ID: dto.ID, + Mode: domain.FocusModeFromAPIType(dto.Type), + Status: domain.FocusStatus(dto.Status), + Title: dto.Note, + TaskID: dto.TaskID, + StartDate: parseTickTime(dto.StartTime), + EndDate: parseTickTime(dto.EndTime), } } -func (c *Client) GetFocus(ctx context.Context, token, focusID string) (domain.Focus, error) { +func (c *Client) GetFocus(ctx context.Context, token, focusID string, focusType int) (domain.Focus, error) { + path := fmt.Sprintf("/open/v1/focus/%s?type=%d", focusID, focusType) var dto focusDTO - if err := c.DoJSON(ctx, http.MethodGet, "/open/v1/focus/"+focusID, token, nil, &dto); err != nil { + if err := c.DoJSON(ctx, http.MethodGet, path, token, nil, &dto); err != nil { return domain.Focus{}, err } return mapFocus(dto), nil } -func (c *Client) ListFocus(ctx context.Context, token string, startDate, endDate time.Time) ([]domain.Focus, error) { - path := fmt.Sprintf("/open/v1/focus?from=%s&to=%s", - startDate.Format(time.RFC3339), - endDate.Format(time.RFC3339)) +func (c *Client) ListFocus(ctx context.Context, token string, startDate, endDate time.Time, focusType int) ([]domain.Focus, error) { + const apiTimeFormat = "2006-01-02T15:04:05-0700" + path := fmt.Sprintf("/open/v1/focus?from=%s&to=%s&type=%d", + startDate.Format(apiTimeFormat), + endDate.Format(apiTimeFormat), + focusType) var resp focusListResponse if err := c.DoJSON(ctx, http.MethodGet, path, token, nil, &resp); err != nil { return nil, err @@ -71,25 +62,3 @@ func (c *Client) ListFocus(ctx context.Context, token string, startDate, endDate } return out, nil } - -func (c *Client) StartFocus(ctx context.Context, token string, in domain.StartFocusInput) (domain.Focus, error) { - body := map[string]any{ - "title": in.Title, - "content": in.Content, - "mode": int(in.Mode), - "projectId": in.ProjectID, - "taskId": in.TaskID, - } - if in.StartDate != nil { - body["startDate"] = in.StartDate.Format(time.RFC3339) - } - var dto focusDTO - if err := c.DoJSON(ctx, http.MethodPost, "/open/v1/focus", token, body, &dto); err != nil { - return domain.Focus{}, err - } - return mapFocus(dto), nil -} - -func (c *Client) StopFocus(ctx context.Context, token, focusID string) error { - return c.DoJSON(ctx, http.MethodPost, "/open/v1/focus/"+focusID, token, nil, nil) -} diff --git a/internal/ticktick/focus_test.go b/internal/ticktick/focus_test.go index a2e0b4b..62b45b5 100644 --- a/internal/ticktick/focus_test.go +++ b/internal/ticktick/focus_test.go @@ -19,13 +19,16 @@ func TestGetFocus(t *testing.T) { if got, want := r.URL.Path, "/open/v1/focus/f1"; got != want { t.Fatalf("Path = %q, want %q", got, want) } - resp := focusDTO{ID: "f1", Title: "Focus 1", Mode: 1, Status: 0} + if got, want := r.URL.Query().Get("type"), "1"; got != want { + t.Fatalf("type = %q, want %q", got, want) + } + resp := focusDTO{ID: "f1", Note: "Focus 1", Type: 1, Status: 0} _ = json.NewEncoder(w).Encode(resp) })) defer server.Close() client := New(server.URL, server.Client()) - focus, err := client.GetFocus(context.Background(), "token", "f1") + focus, err := client.GetFocus(context.Background(), "token", "f1", 1) if err != nil { t.Fatalf("GetFocus() error = %v", err) } @@ -45,8 +48,11 @@ func TestListFocus(t *testing.T) { if got, want := r.Method, http.MethodGet; got != want { t.Fatalf("Method = %q, want %q", got, want) } + if got, want := r.URL.Query().Get("type"), "0"; got != want { + t.Fatalf("type = %q, want %q", got, want) + } resp := focusListResponse{Focuses: []focusDTO{ - {ID: "f1", Title: "Focus 1", Mode: 1, Status: 0}, + {ID: "f1", Note: "Focus 1", Type: 0, Status: 0}, }} _ = json.NewEncoder(w).Encode(resp) })) @@ -55,61 +61,15 @@ func TestListFocus(t *testing.T) { client := New(server.URL, server.Client()) start := time.Now() end := start.Add(24 * time.Hour) - focuses, err := client.ListFocus(context.Background(), "token", start, end) + focuses, err := client.ListFocus(context.Background(), "token", start, end, 0) if err != nil { t.Fatalf("ListFocus() error = %v", err) } if len(focuses) != 1 { t.Fatalf("len(focuses) = %d, want 1", len(focuses)) } -} - -func TestStartFocus(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got, want := r.Method, http.MethodPost; got != want { - t.Fatalf("Method = %q, want %q", got, want) - } - if got, want := r.URL.Path, "/open/v1/focus"; got != want { - t.Fatalf("Path = %q, want %q", got, want) - } - resp := focusDTO{ID: "f2", Title: "New Focus", Mode: 2, Status: 0} - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := New(server.URL, server.Client()) - start := time.Now() - focus, err := client.StartFocus(context.Background(), "token", domain.StartFocusInput{ - Title: "New Focus", - Mode: domain.FocusModePomodoro, - StartDate: &start, - }) - if err != nil { - t.Fatalf("StartFocus() error = %v", err) - } - if focus.ID != "f2" { - t.Fatalf("ID = %q, want f2", focus.ID) - } - if focus.Mode != domain.FocusModePomodoro { - t.Fatalf("Mode = %v, want pomodoro", focus.Mode) - } -} - -func TestStopFocus(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got, want := r.Method, http.MethodPost; got != want { - t.Fatalf("Method = %q, want %q", got, want) - } - if got, want := r.URL.Path, "/open/v1/focus/f1"; got != want { - t.Fatalf("Path = %q, want %q", got, want) - } - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - client := New(server.URL, server.Client()) - if err := client.StopFocus(context.Background(), "token", "f1"); err != nil { - t.Fatalf("StopFocus() error = %v", err) + if focuses[0].Mode != domain.FocusModePomodoro { + t.Fatalf("Mode = %v, want pomodoro", focuses[0].Mode) } } @@ -120,7 +80,7 @@ func TestGetFocusReturnsError(t *testing.T) { defer server.Close() client := New(server.URL, server.Client()) - _, err := client.GetFocus(context.Background(), "token", "f1") + _, err := client.GetFocus(context.Background(), "token", "f1", 1) if err == nil { t.Fatal("GetFocus() error = nil, want error") } @@ -130,22 +90,25 @@ func TestMapFocus(t *testing.T) { now := time.Now() dto := focusDTO{ ID: "f1", - Mode: 1, + Type: 0, Status: 0, - Title: "Focus", - ProjectID: "p1", - StartDate: now.Format("2006-01-02T15:04:05.000-0700"), + Note: "Focus", + TaskID: "t1", + StartTime: now.Format("2006-01-02T15:04:05.000-0700"), } f := mapFocus(dto) if f.ID != "f1" { t.Fatalf("ID = %q, want f1", f.ID) } - if f.Mode != domain.FocusModeTimer { - t.Fatalf("Mode = %v, want timer", f.Mode) + if f.Mode != domain.FocusModePomodoro { + t.Fatalf("Mode = %v, want pomodoro", f.Mode) } if f.Status != domain.FocusStatusActive { t.Fatalf("Status = %v, want active", f.Status) } + if f.Title != "Focus" { + t.Fatalf("Title = %q, want Focus", f.Title) + } if f.StartDate == nil { t.Fatal("StartDate = nil, want non-nil") }