This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / pages.go
45 kB 1718 lines
1package pages 2 3import ( 4 "crypto/sha256" 5 "embed" 6 "encoding/hex" 7 "fmt" 8 "html/template" 9 "io" 10 "io/fs" 11 "log/slog" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 "sync" 17 "time" 18 19 "tangled.org/core/api/tangled" 20 "tangled.org/core/appview/cache" 21 "tangled.org/core/appview/commitverify" 22 "tangled.org/core/appview/config" 23 "tangled.org/core/appview/db" 24 "tangled.org/core/appview/models" 25 "tangled.org/core/appview/oauth" 26 "tangled.org/core/appview/pages/markup" 27 "tangled.org/core/appview/pages/repoinfo" 28 "tangled.org/core/appview/pagination" 29 "tangled.org/core/idresolver" 30 "tangled.org/core/patchutil" 31 "tangled.org/core/types" 32 33 "github.com/bluesky-social/indigo/atproto/identity" 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 "github.com/go-git/go-git/v5/plumbing" 36) 37 38//go:embed templates/* static legal 39var Files embed.FS 40 41type Pages struct { 42 mu sync.RWMutex 43 cache *TmplCache[string, *template.Template] 44 45 avatar config.AvatarConfig 46 pdsCfg config.PdsConfig 47 resolver *idresolver.Resolver 48 db *db.DB 49 rdb *cache.Cache 50 dev bool 51 embedFS fs.FS 52 templateDir string // Path to templates on disk for dev mode 53 rctx *markup.RenderContext 54 logger *slog.Logger 55} 56 57func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages { 58 // initialized with safe defaults, can be overridden per use 59 rctx := &markup.RenderContext{ 60 IsDev: config.Core.Dev, 61 Hostname: config.Core.AppviewHost, 62 CamoUrl: config.Camo.Host, 63 CamoSecret: config.Camo.SharedSecret, 64 Sanitizer: markup.NewSanitizer(), 65 Files: Files, 66 } 67 68 p := &Pages{ 69 mu: sync.RWMutex{}, 70 cache: NewTmplCache[string, *template.Template](), 71 dev: config.Core.Dev, 72 avatar: config.Avatar, 73 pdsCfg: config.Pds, 74 rctx: rctx, 75 resolver: res, 76 db: database, 77 rdb: rdb, 78 templateDir: "appview/pages", 79 logger: logger, 80 } 81 82 if p.dev { 83 p.embedFS = os.DirFS(p.templateDir) 84 } else { 85 p.embedFS = Files 86 } 87 88 return p 89} 90 91// reverse of pathToName 92func (p *Pages) nameToPath(s string) string { 93 return "templates/" + s + ".html" 94} 95 96// FuncMap returns the template function map for use by external template consumers. 97func (p *Pages) FuncMap() template.FuncMap { 98 return p.funcMap() 99} 100 101// FragmentPaths returns all fragment template paths from the embedded FS. 102func (p *Pages) FragmentPaths() ([]string, error) { 103 return p.fragmentPaths() 104} 105 106// EmbedFS returns the embedded filesystem containing templates and static assets. 107func (p *Pages) EmbedFS() fs.FS { 108 return p.embedFS 109} 110 111// ParseWith parses the base layout together with all appview fragments and 112// an additional template from extraFS identified by extraPath (relative to 113// extraFS root). The returned template is ready to ExecuteTemplate with 114// "layouts/base" -- primarily for use with the blog. 115func (p *Pages) ParseWith(extraFS fs.FS, extraPath string) (*template.Template, error) { 116 fragmentPaths, err := p.fragmentPaths() 117 if err != nil { 118 return nil, err 119 } 120 121 funcs := p.funcMap() 122 tpl, err := template.New("layouts/base"). 123 Funcs(funcs). 124 ParseFS(p.embedFS, append(fragmentPaths, p.nameToPath("layouts/base"))...) 125 if err != nil { 126 return nil, err 127 } 128 129 err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error { 130 if err != nil { 131 return err 132 } 133 if d.IsDir() || !strings.HasSuffix(path, ".html") { 134 return nil 135 } 136 if path != extraPath && !strings.Contains(path, "fragments/") { 137 return nil 138 } 139 data, err := fs.ReadFile(extraFS, path) 140 if err != nil { 141 return err 142 } 143 if _, err = tpl.New(path).Parse(string(data)); err != nil { 144 return err 145 } 146 return nil 147 }) 148 if err != nil { 149 return nil, err 150 } 151 152 return tpl, nil 153} 154 155func (p *Pages) fragmentPaths() ([]string, error) { 156 var fragmentPaths []string 157 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 158 if err != nil { 159 return err 160 } 161 if d.IsDir() { 162 return nil 163 } 164 if !strings.HasSuffix(path, ".html") { 165 return nil 166 } 167 if !strings.Contains(path, "fragments/") { 168 return nil 169 } 170 fragmentPaths = append(fragmentPaths, path) 171 return nil 172 }) 173 if err != nil { 174 return nil, err 175 } 176 177 return fragmentPaths, nil 178} 179 180// parse without memoization 181func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 182 paths, err := p.fragmentPaths() 183 if err != nil { 184 return nil, err 185 } 186 for _, s := range stack { 187 paths = append(paths, p.nameToPath(s)) 188 } 189 190 funcs := p.funcMap() 191 top := stack[len(stack)-1] 192 parsed, err := template.New(top). 193 Funcs(funcs). 194 ParseFS(p.embedFS, paths...) 195 if err != nil { 196 return nil, err 197 } 198 199 return parsed, nil 200} 201 202func (p *Pages) parse(stack ...string) (*template.Template, error) { 203 key := strings.Join(stack, "|") 204 205 // never cache in dev mode 206 if cached, exists := p.cache.Get(key); !p.dev && exists { 207 return cached, nil 208 } 209 210 result, err := p.rawParse(stack...) 211 if err != nil { 212 return nil, err 213 } 214 215 p.cache.Set(key, result) 216 return result, nil 217} 218 219func (p *Pages) parseBase(top string) (*template.Template, error) { 220 stack := []string{ 221 "layouts/base", 222 top, 223 } 224 return p.parse(stack...) 225} 226 227func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 228 stack := []string{ 229 "layouts/base", 230 "layouts/repobase", 231 top, 232 } 233 return p.parse(stack...) 234} 235 236func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 237 stack := []string{ 238 "layouts/base", 239 "layouts/profilebase", 240 top, 241 } 242 return p.parse(stack...) 243} 244 245func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 246 stack := []string{ 247 "layouts/base", 248 "layouts/loginbase", 249 top, 250 } 251 return p.parse(stack...) 252} 253 254func (p *Pages) executePlain(name string, w io.Writer, params any) error { 255 tpl, err := p.parse(name) 256 if err != nil { 257 return err 258 } 259 260 return tpl.Execute(w, params) 261} 262 263func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 264 tpl, err := p.parseLoginBase(name) 265 if err != nil { 266 return err 267 } 268 269 return tpl.ExecuteTemplate(w, "layouts/base", params) 270} 271 272func (p *Pages) execute(name string, w io.Writer, params any) error { 273 tpl, err := p.parseBase(name) 274 if err != nil { 275 return err 276 } 277 278 return tpl.ExecuteTemplate(w, "layouts/base", params) 279} 280 281func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 282 tpl, err := p.parseRepoBase(name) 283 if err != nil { 284 return err 285 } 286 287 return tpl.ExecuteTemplate(w, "layouts/base", params) 288} 289 290func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 291 tpl, err := p.parseProfileBase(name) 292 if err != nil { 293 return err 294 } 295 296 return tpl.ExecuteTemplate(w, "layouts/base", params) 297} 298 299type DollyParams struct { 300 Classes string 301 FillColor string 302} 303 304func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 305 return p.executePlain("fragments/dolly/logo", w, params) 306} 307 308func (p *Pages) Favicon(w io.Writer) error { 309 return p.Dolly(w, DollyParams{ 310 Classes: "text-black dark:text-white", 311 }) 312} 313 314type LoginParams struct { 315 ReturnUrl string 316 ErrorCode string 317 AddAccount bool 318 Accounts []oauth.AccountInfo 319} 320 321func (p *Pages) Login(w io.Writer, params LoginParams) error { 322 return p.executeLogin("user/login", w, params) 323} 324 325type SignupParams struct { 326 CloudflareSiteKey string 327 EmailId string 328} 329 330func (p *Pages) Signup(w io.Writer, params SignupParams) error { 331 return p.executeLogin("user/signup", w, params) 332} 333 334func (p *Pages) CompleteSignup(w io.Writer) error { 335 return p.executeLogin("user/completeSignup", w, nil) 336} 337 338type TermsOfServiceParams struct { 339 LoggedInUser *oauth.MultiAccountUser 340 Content template.HTML 341} 342 343func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 344 filename := "terms.md" 345 filePath := filepath.Join("legal", filename) 346 347 file, err := p.embedFS.Open(filePath) 348 if err != nil { 349 return fmt.Errorf("failed to read %s: %w", filename, err) 350 } 351 defer file.Close() 352 353 markdownBytes, err := io.ReadAll(file) 354 if err != nil { 355 return fmt.Errorf("failed to read %s: %w", filename, err) 356 } 357 358 rctx := p.rctx.Clone() 359 rctx.RendererType = markup.RendererTypeDefault 360 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 361 sanitized := rctx.SanitizeDefault(htmlString) 362 params.Content = template.HTML(sanitized) 363 364 return p.execute("legal/terms", w, params) 365} 366 367type PrivacyPolicyParams struct { 368 LoggedInUser *oauth.MultiAccountUser 369 Content template.HTML 370} 371 372func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 373 filename := "privacy.md" 374 filePath := filepath.Join("legal", filename) 375 376 file, err := p.embedFS.Open(filePath) 377 if err != nil { 378 return fmt.Errorf("failed to read %s: %w", filename, err) 379 } 380 defer file.Close() 381 382 markdownBytes, err := io.ReadAll(file) 383 if err != nil { 384 return fmt.Errorf("failed to read %s: %w", filename, err) 385 } 386 387 rctx := p.rctx.Clone() 388 rctx.RendererType = markup.RendererTypeDefault 389 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 390 sanitized := rctx.SanitizeDefault(htmlString) 391 params.Content = template.HTML(sanitized) 392 393 return p.execute("legal/privacy", w, params) 394} 395 396type BrandParams struct { 397 LoggedInUser *oauth.MultiAccountUser 398} 399 400func (p *Pages) Brand(w io.Writer, params BrandParams) error { 401 return p.execute("brand/brand", w, params) 402} 403 404type TimelineParams struct { 405 LoggedInUser *oauth.MultiAccountUser 406 Timeline []models.TimelineEvent 407 Repos []models.Repo 408 GfiLabel *models.LabelDefinition 409 BlueskyPosts []models.BskyPost 410 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 411 // For logged-in users it reflects their newsletter_preferences row; for 412 // anonymous visitors it is always true (dismissal falls back to 413 // localStorage on the client). 414 ShowNewsletter bool 415} 416 417func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 418 return p.execute("timeline/timeline", w, params) 419} 420 421type GoodFirstIssuesParams struct { 422 LoggedInUser *oauth.MultiAccountUser 423 Issues []models.Issue 424 RepoGroups []*models.RepoGroup 425 LabelDefs map[string]*models.LabelDefinition 426 GfiLabel *models.LabelDefinition 427 Page pagination.Page 428} 429 430func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 431 return p.execute("goodfirstissues/index", w, params) 432} 433 434type UserProfileSettingsParams struct { 435 LoggedInUser *oauth.MultiAccountUser 436 Tab string 437 PunchcardPreference models.PunchcardPreference 438 IsTnglSh bool 439 IsDeactivated bool 440 HandleOpen bool 441} 442 443func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 444 params.Tab = "profile" 445 return p.execute("user/settings/profile", w, params) 446} 447 448type NotificationsParams struct { 449 LoggedInUser *oauth.MultiAccountUser 450 Notifications []*models.NotificationWithEntity 451 UnreadCount int 452 Page pagination.Page 453 Total int64 454} 455 456func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 457 return p.execute("notifications/list", w, params) 458} 459 460type NotificationItemParams struct { 461 Notification *models.Notification 462} 463 464func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 465 return p.executePlain("notifications/fragments/item", w, params) 466} 467 468type NotificationCountParams struct { 469 Count int64 470} 471 472func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 473 return p.executePlain("notifications/fragments/count", w, params) 474} 475 476type UserKeysSettingsParams struct { 477 LoggedInUser *oauth.MultiAccountUser 478 PubKeys []models.PublicKey 479 Tab string 480} 481 482func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 483 params.Tab = "keys" 484 return p.execute("user/settings/keys", w, params) 485} 486 487type UserEmailsSettingsParams struct { 488 LoggedInUser *oauth.MultiAccountUser 489 Emails []models.Email 490 Tab string 491} 492 493func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 494 params.Tab = "emails" 495 return p.execute("user/settings/emails", w, params) 496} 497 498type UserNotificationSettingsParams struct { 499 LoggedInUser *oauth.MultiAccountUser 500 Preferences *models.NotificationPreferences 501 Tab string 502} 503 504func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 505 params.Tab = "notifications" 506 return p.execute("user/settings/notifications", w, params) 507} 508 509type UserSiteSettingsParams struct { 510 LoggedInUser *oauth.MultiAccountUser 511 Claim *models.DomainClaim 512 SitesDomain string 513 IsTnglHandle bool 514 Tab string 515} 516 517func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 518 params.Tab = "sites" 519 return p.execute("user/settings/sites", w, params) 520} 521 522type UpgradeBannerParams struct { 523 Registrations []models.Registration 524 Spindles []models.Spindle 525} 526 527func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 528 return p.executePlain("banner", w, params) 529} 530 531type NewsletterResponseParams struct { 532 // Id identifies the calling form instance; the response span's id will 533 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target. 534 Id string 535 // Error, when non-empty, switches the template to the error variant. 536 Error string 537} 538 539func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error { 540 return p.executePlain("timeline/fragments/newsletterResponse", w, params) 541} 542 543type KnotsParams struct { 544 LoggedInUser *oauth.MultiAccountUser 545 Registrations []models.Registration 546 Tab string 547} 548 549func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 550 params.Tab = "knots" 551 return p.execute("knots/index", w, params) 552} 553 554type KnotParams struct { 555 LoggedInUser *oauth.MultiAccountUser 556 Registration *models.Registration 557 Members []string 558 Repos map[string][]models.Repo 559 IsOwner bool 560 Tab string 561} 562 563func (p *Pages) Knot(w io.Writer, params KnotParams) error { 564 return p.execute("knots/dashboard", w, params) 565} 566 567type KnotListingParams struct { 568 *models.Registration 569} 570 571func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 572 return p.executePlain("knots/fragments/knotListing", w, params) 573} 574 575type SpindlesParams struct { 576 LoggedInUser *oauth.MultiAccountUser 577 Spindles []models.Spindle 578 Tab string 579} 580 581func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 582 params.Tab = "spindles" 583 return p.execute("spindles/index", w, params) 584} 585 586type SpindleListingParams struct { 587 models.Spindle 588 Tab string 589} 590 591func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 592 return p.executePlain("spindles/fragments/spindleListing", w, params) 593} 594 595type SpindleDashboardParams struct { 596 LoggedInUser *oauth.MultiAccountUser 597 Spindle models.Spindle 598 Members []string 599 Repos map[string][]models.Repo 600 Tab string 601} 602 603func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 604 return p.execute("spindles/dashboard", w, params) 605} 606 607type NewRepoParams struct { 608 LoggedInUser *oauth.MultiAccountUser 609 Knots []string 610} 611 612func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 613 return p.execute("repo/new", w, params) 614} 615 616type ForkRepoParams struct { 617 LoggedInUser *oauth.MultiAccountUser 618 Knots []string 619 RepoInfo repoinfo.RepoInfo 620} 621 622func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 623 return p.execute("repo/fork", w, params) 624} 625 626type ProfileCard struct { 627 UserDid string 628 HasProfile bool 629 FollowStatus models.FollowStatus 630 Punchcard *models.Punchcard 631 Profile *models.Profile 632 Stats ProfileStats 633 Active string 634} 635 636type ProfileStats struct { 637 RepoCount int64 638 StarredCount int64 639 StringCount int64 640 FollowersCount int64 641 FollowingCount int64 642} 643 644func (p *ProfileCard) GetTabs() [][]any { 645 tabs := [][]any{ 646 {"overview", "overview", "square-chart-gantt", nil}, 647 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 648 {"starred", "starred", "star", p.Stats.StarredCount}, 649 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 650 } 651 652 return tabs 653} 654 655type ProfileOverviewParams struct { 656 LoggedInUser *oauth.MultiAccountUser 657 Repos []models.Repo 658 CollaboratingRepos []models.Repo 659 ProfileTimeline *models.ProfileTimeline 660 Card *ProfileCard 661 Active string 662 ShowPunchcard bool 663} 664 665func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 666 params.Active = "overview" 667 return p.executeProfile("user/overview", w, params) 668} 669 670type ProfileReposParams struct { 671 LoggedInUser *oauth.MultiAccountUser 672 Repos []models.Repo 673 Card *ProfileCard 674 Active string 675 Page pagination.Page 676 RepoCount int 677 FilterQuery string 678} 679 680func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 681 params.Active = "repos" 682 return p.executeProfile("user/repos", w, params) 683} 684 685type ProfileStarredParams struct { 686 LoggedInUser *oauth.MultiAccountUser 687 Repos []models.Repo 688 Card *ProfileCard 689 Page pagination.Page 690 Total int 691 Active string 692} 693 694func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 695 params.Active = "starred" 696 return p.executeProfile("user/starred", w, params) 697} 698 699type ProfileStringsParams struct { 700 LoggedInUser *oauth.MultiAccountUser 701 Strings []models.String 702 Card *ProfileCard 703 Active string 704} 705 706func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 707 params.Active = "strings" 708 return p.executeProfile("user/strings", w, params) 709} 710 711type FollowCard struct { 712 UserDid string 713 LoggedInUser *oauth.MultiAccountUser 714 FollowStatus models.FollowStatus 715 FollowersCount int64 716 FollowingCount int64 717 Profile *models.Profile 718} 719 720type ProfileFollowersParams struct { 721 LoggedInUser *oauth.MultiAccountUser 722 Followers []FollowCard 723 Card *ProfileCard 724 Active string 725} 726 727func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 728 params.Active = "overview" 729 return p.executeProfile("user/followers", w, params) 730} 731 732type ProfileFollowingParams struct { 733 LoggedInUser *oauth.MultiAccountUser 734 Following []FollowCard 735 Card *ProfileCard 736 Active string 737} 738 739func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 740 params.Active = "overview" 741 return p.executeProfile("user/following", w, params) 742} 743 744type FollowFragmentParams struct { 745 UserDid string 746 FollowStatus models.FollowStatus 747 FollowersCount int64 748} 749 750func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 751 return p.executePlain("user/fragments/follow-oob", w, params) 752} 753 754type EditBioParams struct { 755 LoggedInUser *oauth.MultiAccountUser 756 Profile *models.Profile 757 AlsoKnownAs []string 758} 759 760func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 761 return p.executePlain("user/fragments/editBio", w, params) 762} 763 764type EditPinsParams struct { 765 LoggedInUser *oauth.MultiAccountUser 766 Profile *models.Profile 767 AllRepos []PinnedRepo 768} 769 770type PinnedRepo struct { 771 IsPinned bool 772 models.Repo 773} 774 775func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 776 return p.executePlain("user/fragments/editPins", w, params) 777} 778 779type StarBtnFragmentParams struct { 780 IsStarred bool 781 SubjectAt syntax.ATURI 782 StarCount int 783 RepoName string 784 HxSwapOob bool 785} 786 787func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 788 params.HxSwapOob = true 789 return p.executePlain("fragments/starBtn", w, params) 790} 791 792type RepoIndexParams struct { 793 LoggedInUser *oauth.MultiAccountUser 794 RepoInfo repoinfo.RepoInfo 795 Active string 796 TagMap map[string][]string 797 CommitsTrunc []types.Commit 798 TagsTrunc []*types.TagReference 799 BranchesTrunc []types.Branch 800 // ForkInfo *types.ForkInfo 801 HTMLReadme template.HTML 802 Raw bool 803 EmailToDid map[string]string 804 VerifiedCommits commitverify.VerifiedCommits 805 Languages []types.RepoLanguageDetails 806 Pipelines map[string]models.Pipeline 807 NeedsKnotUpgrade bool 808 KnotUnreachable bool 809 types.RepoIndexResponse 810} 811 812func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 813 params.Active = "overview" 814 if params.IsEmpty { 815 return p.executeRepo("repo/empty", w, params) 816 } 817 818 if params.NeedsKnotUpgrade { 819 return p.executeRepo("repo/needsUpgrade", w, params) 820 } 821 822 if params.KnotUnreachable { 823 return p.executeRepo("repo/knotUnreachable", w, params) 824 } 825 826 rctx := p.rctx.Clone() 827 rctx.RepoInfo = params.RepoInfo 828 rctx.RepoInfo.Ref = params.Ref 829 rctx.RendererType = markup.RendererTypeRepoMarkdown 830 831 if params.ReadmeFileName != "" { 832 ext := filepath.Ext(params.ReadmeFileName) 833 switch ext { 834 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 835 params.Raw = false 836 htmlString := rctx.RenderMarkdown(params.Readme) 837 sanitized := rctx.SanitizeDefault(htmlString) 838 params.HTMLReadme = template.HTML(sanitized) 839 default: 840 params.Raw = true 841 } 842 } 843 844 return p.executeRepo("repo/index", w, params) 845} 846 847type RepoLogParams struct { 848 LoggedInUser *oauth.MultiAccountUser 849 RepoInfo repoinfo.RepoInfo 850 TagMap map[string][]string 851 Active string 852 EmailToDid map[string]string 853 VerifiedCommits commitverify.VerifiedCommits 854 Pipelines map[string]models.Pipeline 855 856 types.RepoLogResponse 857} 858 859func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 860 params.Active = "overview" 861 return p.executeRepo("repo/log", w, params) 862} 863 864type RepoCommitParams struct { 865 LoggedInUser *oauth.MultiAccountUser 866 RepoInfo repoinfo.RepoInfo 867 Active string 868 EmailToDid map[string]string 869 Pipeline *models.Pipeline 870 DiffOpts types.DiffOpts 871 872 // singular because it's always going to be just one 873 VerifiedCommit commitverify.VerifiedCommits 874 875 types.RepoCommitResponse 876} 877 878func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 879 params.Active = "overview" 880 return p.executeRepo("repo/commit", w, params) 881} 882 883type RepoTreeParams struct { 884 LoggedInUser *oauth.MultiAccountUser 885 RepoInfo repoinfo.RepoInfo 886 Active string 887 BreadCrumbs [][]string 888 Path string 889 Raw bool 890 HTMLReadme template.HTML 891 EmailToDid map[string]string 892 LastCommitInfo *types.LastCommitInfo 893 types.RepoTreeResponse 894} 895 896type RepoTreeStats struct { 897 NumFolders uint64 898 NumFiles uint64 899} 900 901func (r RepoTreeParams) TreeStats() RepoTreeStats { 902 numFolders, numFiles := 0, 0 903 for _, f := range r.Files { 904 if !f.IsFile() { 905 numFolders += 1 906 } else if f.IsFile() { 907 numFiles += 1 908 } 909 } 910 911 return RepoTreeStats{ 912 NumFolders: uint64(numFolders), 913 NumFiles: uint64(numFiles), 914 } 915} 916 917func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 918 params.Active = "overview" 919 920 rctx := p.rctx.Clone() 921 rctx.RepoInfo = params.RepoInfo 922 rctx.RepoInfo.Ref = params.Ref 923 rctx.RendererType = markup.RendererTypeRepoMarkdown 924 925 if params.ReadmeFileName != "" { 926 ext := filepath.Ext(params.ReadmeFileName) 927 switch ext { 928 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 929 params.Raw = false 930 htmlString := rctx.RenderMarkdown(params.Readme) 931 sanitized := rctx.SanitizeDefault(htmlString) 932 params.HTMLReadme = template.HTML(sanitized) 933 default: 934 params.Raw = true 935 } 936 } 937 938 return p.executeRepo("repo/tree", w, params) 939} 940 941type RepoBranchesParams struct { 942 LoggedInUser *oauth.MultiAccountUser 943 RepoInfo repoinfo.RepoInfo 944 Active string 945 types.RepoBranchesResponse 946} 947 948func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 949 params.Active = "overview" 950 return p.executeRepo("repo/branches", w, params) 951} 952 953type RepoTagsParams struct { 954 LoggedInUser *oauth.MultiAccountUser 955 RepoInfo repoinfo.RepoInfo 956 Active string 957 types.RepoTagsResponse 958 ArtifactMap map[plumbing.Hash][]models.Artifact 959 DanglingArtifacts []models.Artifact 960} 961 962func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 963 params.Active = "overview" 964 return p.executeRepo("repo/tags", w, params) 965} 966 967type RepoTagParams struct { 968 LoggedInUser *oauth.MultiAccountUser 969 RepoInfo repoinfo.RepoInfo 970 Active string 971 types.RepoTagResponse 972 ArtifactMap map[plumbing.Hash][]models.Artifact 973 DanglingArtifacts []models.Artifact 974} 975 976func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 977 params.Active = "overview" 978 return p.executeRepo("repo/tag", w, params) 979} 980 981type RepoArtifactParams struct { 982 LoggedInUser *oauth.MultiAccountUser 983 RepoInfo repoinfo.RepoInfo 984 Artifact models.Artifact 985} 986 987func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 988 return p.executePlain("repo/fragments/artifact", w, params) 989} 990 991type RepoBlobParams struct { 992 LoggedInUser *oauth.MultiAccountUser 993 RepoInfo repoinfo.RepoInfo 994 Active string 995 BreadCrumbs [][]string 996 BlobView models.BlobView 997 EmailToDid map[string]string 998 LastCommitInfo *types.LastCommitInfo 999 *tangled.RepoBlob_Output 1000} 1001 1002func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1003 params.Active = "overview" 1004 return p.executeRepo("repo/blob", w, params) 1005} 1006 1007type Collaborator struct { 1008 Did string 1009 Role string 1010} 1011 1012type RepoSettingsParams struct { 1013 LoggedInUser *oauth.MultiAccountUser 1014 RepoInfo repoinfo.RepoInfo 1015 Collaborators []Collaborator 1016 Active string 1017 Branches []types.Branch 1018 Spindles []string 1019 CurrentSpindle string 1020 Secrets []*tangled.RepoListSecrets_Secret 1021 1022 // TODO: use repoinfo.roles 1023 IsCollaboratorInviteAllowed bool 1024} 1025 1026func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1027 params.Active = "settings" 1028 return p.executeRepo("repo/settings", w, params) 1029} 1030 1031type RepoGeneralSettingsParams struct { 1032 LoggedInUser *oauth.MultiAccountUser 1033 RepoInfo repoinfo.RepoInfo 1034 Labels []models.LabelDefinition 1035 DefaultLabels []models.LabelDefinition 1036 SubscribedLabels map[string]struct{} 1037 ShouldSubscribeAll bool 1038 Active string 1039 Tab string 1040 Branches []types.Branch 1041} 1042 1043func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1044 params.Active = "settings" 1045 params.Tab = "general" 1046 return p.executeRepo("repo/settings/general", w, params) 1047} 1048 1049type RepoAccessSettingsParams struct { 1050 LoggedInUser *oauth.MultiAccountUser 1051 RepoInfo repoinfo.RepoInfo 1052 Active string 1053 Tab string 1054 Collaborators []Collaborator 1055} 1056 1057func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1058 params.Active = "settings" 1059 params.Tab = "access" 1060 return p.executeRepo("repo/settings/access", w, params) 1061} 1062 1063type RepoPipelineSettingsParams struct { 1064 LoggedInUser *oauth.MultiAccountUser 1065 RepoInfo repoinfo.RepoInfo 1066 Active string 1067 Tab string 1068 Spindles []string 1069 CurrentSpindle string 1070 Secrets []map[string]any 1071} 1072 1073func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1074 params.Active = "settings" 1075 params.Tab = "pipelines" 1076 return p.executeRepo("repo/settings/pipelines", w, params) 1077} 1078 1079type RepoWebhooksSettingsParams struct { 1080 LoggedInUser *oauth.MultiAccountUser 1081 RepoInfo repoinfo.RepoInfo 1082 Active string 1083 Tab string 1084 Webhooks []models.Webhook 1085 WebhookDeliveries map[int64][]models.WebhookDelivery 1086} 1087 1088func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1089 params.Active = "settings" 1090 params.Tab = "hooks" 1091 return p.executeRepo("repo/settings/hooks", w, params) 1092} 1093 1094type WebhookDeliveriesListParams struct { 1095 LoggedInUser *oauth.MultiAccountUser 1096 RepoInfo repoinfo.RepoInfo 1097 Webhook *models.Webhook 1098 Deliveries []models.WebhookDelivery 1099} 1100 1101func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1102 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1103 if err != nil { 1104 return err 1105 } 1106 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1107} 1108 1109type RepoSiteSettingsParams struct { 1110 LoggedInUser *oauth.MultiAccountUser 1111 RepoInfo repoinfo.RepoInfo 1112 Active string 1113 Tab string 1114 Branches []types.Branch 1115 SiteConfig *models.RepoSite 1116 OwnerClaim *models.DomainClaim 1117 Deploys []models.SiteDeploy 1118 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1119} 1120 1121func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1122 params.Active = "settings" 1123 params.Tab = "sites" 1124 return p.executeRepo("repo/settings/sites", w, params) 1125} 1126 1127type RepoIssuesParams struct { 1128 LoggedInUser *oauth.MultiAccountUser 1129 RepoInfo repoinfo.RepoInfo 1130 Active string 1131 Issues []models.Issue 1132 IssueCount int 1133 LabelDefs map[string]*models.LabelDefinition 1134 Page pagination.Page 1135 FilterState string 1136 FilterQuery string 1137} 1138 1139func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1140 params.Active = "issues" 1141 return p.executeRepo("repo/issues/issues", w, params) 1142} 1143 1144type RepoSingleIssueParams struct { 1145 LoggedInUser *oauth.MultiAccountUser 1146 RepoInfo repoinfo.RepoInfo 1147 Active string 1148 Issue *models.Issue 1149 CommentList []models.CommentListItem 1150 Backlinks []models.RichReferenceLink 1151 LabelDefs map[string]*models.LabelDefinition 1152 1153 Reactions map[models.ReactionKind]models.ReactionDisplayData 1154 UserReacted map[models.ReactionKind]bool 1155} 1156 1157func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1158 params.Active = "issues" 1159 return p.executeRepo("repo/issues/issue", w, params) 1160} 1161 1162type EditIssueParams struct { 1163 LoggedInUser *oauth.MultiAccountUser 1164 RepoInfo repoinfo.RepoInfo 1165 Issue *models.Issue 1166 Action string 1167} 1168 1169func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1170 params.Action = "edit" 1171 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1172} 1173 1174type ThreadReactionFragmentParams struct { 1175 ThreadAt syntax.ATURI 1176 Kind models.ReactionKind 1177 Count int 1178 Users []string 1179 IsReacted bool 1180} 1181 1182func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1183 return p.executePlain("repo/fragments/reaction", w, params) 1184} 1185 1186type RepoNewIssueParams struct { 1187 LoggedInUser *oauth.MultiAccountUser 1188 RepoInfo repoinfo.RepoInfo 1189 Issue *models.Issue // existing issue if any -- passed when editing 1190 Active string 1191 Action string 1192} 1193 1194func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1195 params.Active = "issues" 1196 params.Action = "create" 1197 return p.executeRepo("repo/issues/new", w, params) 1198} 1199 1200type EditIssueCommentParams struct { 1201 LoggedInUser *oauth.MultiAccountUser 1202 RepoInfo repoinfo.RepoInfo 1203 Issue *models.Issue 1204 Comment *models.IssueComment 1205} 1206 1207func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1208 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1209} 1210 1211type ReplyIssueCommentPlaceholderParams struct { 1212 LoggedInUser *oauth.MultiAccountUser 1213 RepoInfo repoinfo.RepoInfo 1214 Issue *models.Issue 1215 Comment *models.IssueComment 1216} 1217 1218func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1219 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1220} 1221 1222type ReplyIssueCommentParams struct { 1223 LoggedInUser *oauth.MultiAccountUser 1224 RepoInfo repoinfo.RepoInfo 1225 Issue *models.Issue 1226 Comment *models.IssueComment 1227} 1228 1229func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1230 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1231} 1232 1233type IssueCommentBodyParams struct { 1234 LoggedInUser *oauth.MultiAccountUser 1235 RepoInfo repoinfo.RepoInfo 1236 Issue *models.Issue 1237 Comment *models.IssueComment 1238} 1239 1240func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1241 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1242} 1243 1244type RepoNewPullParams struct { 1245 LoggedInUser *oauth.MultiAccountUser 1246 RepoInfo repoinfo.RepoInfo 1247 Branches []types.Branch 1248 Strategy string 1249 SourceBranch string 1250 TargetBranch string 1251 Title string 1252 Body string 1253 Active string 1254} 1255 1256func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1257 params.Active = "pulls" 1258 return p.executeRepo("repo/pulls/new", w, params) 1259} 1260 1261type RepoPullsParams struct { 1262 LoggedInUser *oauth.MultiAccountUser 1263 RepoInfo repoinfo.RepoInfo 1264 Pulls []*models.Pull 1265 Active string 1266 FilterState string 1267 FilterQuery string 1268 Stacks []models.Stack 1269 Pipelines map[string]models.Pipeline 1270 LabelDefs map[string]*models.LabelDefinition 1271 Page pagination.Page 1272 PullCount int 1273} 1274 1275func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1276 params.Active = "pulls" 1277 return p.executeRepo("repo/pulls/pulls", w, params) 1278} 1279 1280type ResubmitResult uint64 1281 1282const ( 1283 ShouldResubmit ResubmitResult = iota 1284 ShouldNotResubmit 1285 Unknown 1286) 1287 1288func (r ResubmitResult) Yes() bool { 1289 return r == ShouldResubmit 1290} 1291func (r ResubmitResult) No() bool { 1292 return r == ShouldNotResubmit 1293} 1294func (r ResubmitResult) Unknown() bool { 1295 return r == Unknown 1296} 1297 1298type RepoSinglePullParams struct { 1299 LoggedInUser *oauth.MultiAccountUser 1300 RepoInfo repoinfo.RepoInfo 1301 Active string 1302 Pull *models.Pull 1303 Stack models.Stack 1304 Backlinks []models.RichReferenceLink 1305 BranchDeleteStatus *models.BranchDeleteStatus 1306 MergeCheck types.MergeCheckResponse 1307 ResubmitCheck ResubmitResult 1308 Pipelines map[string]models.Pipeline 1309 Diff types.DiffRenderer 1310 DiffOpts types.DiffOpts 1311 ActiveRound int 1312 IsInterdiff bool 1313 1314 Reactions map[models.ReactionKind]models.ReactionDisplayData 1315 UserReacted map[models.ReactionKind]bool 1316 1317 LabelDefs map[string]*models.LabelDefinition 1318} 1319 1320func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1321 params.Active = "pulls" 1322 return p.executeRepo("repo/pulls/pull", w, params) 1323} 1324 1325type RepoPullPatchParams struct { 1326 LoggedInUser *oauth.MultiAccountUser 1327 RepoInfo repoinfo.RepoInfo 1328 Pull *models.Pull 1329 Stack models.Stack 1330 Diff *types.NiceDiff 1331 Round int 1332 Submission *models.PullSubmission 1333 DiffOpts types.DiffOpts 1334} 1335 1336// this name is a mouthful 1337func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1338 return p.execute("repo/pulls/patch", w, params) 1339} 1340 1341type RepoPullInterdiffParams struct { 1342 LoggedInUser *oauth.MultiAccountUser 1343 RepoInfo repoinfo.RepoInfo 1344 Pull *models.Pull 1345 Round int 1346 Interdiff *patchutil.InterdiffResult 1347 DiffOpts types.DiffOpts 1348} 1349 1350// this name is a mouthful 1351func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1352 return p.execute("repo/pulls/interdiff", w, params) 1353} 1354 1355type PullPatchUploadParams struct { 1356 RepoInfo repoinfo.RepoInfo 1357} 1358 1359func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1360 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1361} 1362 1363type PullCompareBranchesParams struct { 1364 RepoInfo repoinfo.RepoInfo 1365 Branches []types.Branch 1366 SourceBranch string 1367} 1368 1369func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1370 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1371} 1372 1373type PullCompareForkParams struct { 1374 RepoInfo repoinfo.RepoInfo 1375 Forks []models.Repo 1376 Selected string 1377} 1378 1379func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1380 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1381} 1382 1383type PullCompareForkBranchesParams struct { 1384 RepoInfo repoinfo.RepoInfo 1385 SourceBranches []types.Branch 1386 TargetBranches []types.Branch 1387} 1388 1389func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1390 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1391} 1392 1393type PullResubmitParams struct { 1394 LoggedInUser *oauth.MultiAccountUser 1395 RepoInfo repoinfo.RepoInfo 1396 Pull *models.Pull 1397 SubmissionId int 1398} 1399 1400func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1401 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1402} 1403 1404type PullActionsParams struct { 1405 LoggedInUser *oauth.MultiAccountUser 1406 RepoInfo repoinfo.RepoInfo 1407 Pull *models.Pull 1408 RoundNumber int 1409 MergeCheck types.MergeCheckResponse 1410 ResubmitCheck ResubmitResult 1411 BranchDeleteStatus *models.BranchDeleteStatus 1412 Stack models.Stack 1413} 1414 1415func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1416 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1417} 1418 1419type PullNewCommentParams struct { 1420 LoggedInUser *oauth.MultiAccountUser 1421 RepoInfo repoinfo.RepoInfo 1422 Pull *models.Pull 1423 RoundNumber int 1424} 1425 1426func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1427 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1428} 1429 1430type RepoCompareParams struct { 1431 LoggedInUser *oauth.MultiAccountUser 1432 RepoInfo repoinfo.RepoInfo 1433 Forks []models.Repo 1434 Branches []types.Branch 1435 Tags []*types.TagReference 1436 Base string 1437 Head string 1438 Diff *types.NiceDiff 1439 DiffOpts types.DiffOpts 1440 1441 Active string 1442} 1443 1444func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1445 params.Active = "overview" 1446 return p.executeRepo("repo/compare/compare", w, params) 1447} 1448 1449type RepoCompareNewParams struct { 1450 LoggedInUser *oauth.MultiAccountUser 1451 RepoInfo repoinfo.RepoInfo 1452 Forks []models.Repo 1453 Branches []types.Branch 1454 Tags []*types.TagReference 1455 Base string 1456 Head string 1457 1458 Active string 1459} 1460 1461func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1462 params.Active = "overview" 1463 return p.executeRepo("repo/compare/new", w, params) 1464} 1465 1466type RepoCompareAllowPullParams struct { 1467 LoggedInUser *oauth.MultiAccountUser 1468 RepoInfo repoinfo.RepoInfo 1469 Base string 1470 Head string 1471} 1472 1473func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1474 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1475} 1476 1477type RepoCompareDiffFragmentParams struct { 1478 Diff types.NiceDiff 1479 DiffOpts types.DiffOpts 1480} 1481 1482func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1483 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1484} 1485 1486type LabelPanelParams struct { 1487 LoggedInUser *oauth.MultiAccountUser 1488 RepoInfo repoinfo.RepoInfo 1489 Defs map[string]*models.LabelDefinition 1490 Subject string 1491 State models.LabelState 1492} 1493 1494func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1495 return p.executePlain("repo/fragments/labelPanel", w, params) 1496} 1497 1498type EditLabelPanelParams struct { 1499 LoggedInUser *oauth.MultiAccountUser 1500 RepoInfo repoinfo.RepoInfo 1501 Defs map[string]*models.LabelDefinition 1502 Subject string 1503 State models.LabelState 1504} 1505 1506func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1507 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1508} 1509 1510type RepoStarsParams struct { 1511 LoggedInUser *oauth.MultiAccountUser 1512 RepoInfo repoinfo.RepoInfo 1513 Active string 1514 Starrers []models.Star 1515} 1516 1517func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1518 params.Active = "overview" 1519 return p.executeRepo("repo/stars", w, params) 1520} 1521 1522type PipelinesParams struct { 1523 LoggedInUser *oauth.MultiAccountUser 1524 RepoInfo repoinfo.RepoInfo 1525 Pipelines []models.Pipeline 1526 Active string 1527 FilterKind string 1528 Total int64 1529} 1530 1531func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1532 params.Active = "pipelines" 1533 return p.executeRepo("repo/pipelines/pipelines", w, params) 1534} 1535 1536type LogBlockParams struct { 1537 Id int 1538 Name string 1539 Command string 1540 Collapsed bool 1541 StartTime time.Time 1542} 1543 1544func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1545 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1546} 1547 1548type LogBlockEndParams struct { 1549 Id int 1550 StartTime time.Time 1551 EndTime time.Time 1552} 1553 1554func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1555 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1556} 1557 1558type LogLineParams struct { 1559 Id int 1560 Content string 1561} 1562 1563func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1564 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1565} 1566 1567type WorkflowParams struct { 1568 LoggedInUser *oauth.MultiAccountUser 1569 RepoInfo repoinfo.RepoInfo 1570 Pipeline models.Pipeline 1571 Workflow string 1572 LogUrl string 1573 Active string 1574} 1575 1576func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1577 params.Active = "pipelines" 1578 return p.executeRepo("repo/pipelines/workflow", w, params) 1579} 1580 1581type PutStringParams struct { 1582 LoggedInUser *oauth.MultiAccountUser 1583 Action string 1584 1585 // this is supplied in the case of editing an existing string 1586 String models.String 1587} 1588 1589func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1590 return p.execute("strings/put", w, params) 1591} 1592 1593type StringsDashboardParams struct { 1594 LoggedInUser *oauth.MultiAccountUser 1595 Card ProfileCard 1596 Strings []models.String 1597} 1598 1599func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1600 return p.execute("strings/dashboard", w, params) 1601} 1602 1603type StringTimelineParams struct { 1604 LoggedInUser *oauth.MultiAccountUser 1605 Strings []models.String 1606} 1607 1608func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1609 return p.execute("strings/timeline", w, params) 1610} 1611 1612type SingleStringParams struct { 1613 LoggedInUser *oauth.MultiAccountUser 1614 ShowRendered bool 1615 RenderToggle bool 1616 RenderedContents template.HTML 1617 String *models.String 1618 Stats models.StringStats 1619 IsStarred bool 1620 StarCount int 1621 Owner identity.Identity 1622} 1623 1624func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1625 return p.execute("strings/string", w, params) 1626} 1627 1628type SearchReposParams struct { 1629 LoggedInUser *oauth.MultiAccountUser 1630 Repos []models.Repo 1631 Page pagination.Page 1632 ResultCount int 1633 FilterQuery string 1634 SortParam string 1635 TimeTaken time.Duration 1636 DocCount int64 1637} 1638 1639func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1640 return p.execute("search/search", w, params) 1641} 1642 1643func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1644 return p.execute("timeline/home", w, params) 1645} 1646 1647func (p *Pages) Static() http.Handler { 1648 if p.dev { 1649 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1650 } 1651 1652 sub, err := fs.Sub(p.embedFS, "static") 1653 if err != nil { 1654 p.logger.Error("no static dir found? that's crazy", "err", err) 1655 panic(err) 1656 } 1657 // Custom handler to apply Cache-Control headers for font files 1658 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1659} 1660 1661func Cache(h http.Handler) http.Handler { 1662 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1663 path := strings.Split(r.URL.Path, "?")[0] 1664 1665 if strings.HasSuffix(path, ".css") { 1666 // on day for css files 1667 w.Header().Set("Cache-Control", "public, max-age=86400") 1668 } else { 1669 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1670 } 1671 h.ServeHTTP(w, r) 1672 }) 1673} 1674 1675func (p *Pages) CssContentHash() string { 1676 cssFile, err := p.embedFS.Open("static/tw.css") 1677 if err != nil { 1678 slog.Debug("Error opening CSS file", "err", err) 1679 return "" 1680 } 1681 defer cssFile.Close() 1682 1683 hasher := sha256.New() 1684 if _, err := io.Copy(hasher, cssFile); err != nil { 1685 slog.Debug("Error hashing CSS file", "err", err) 1686 return "" 1687 } 1688 1689 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1690} 1691 1692func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1693 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1694} 1695 1696func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1697 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1698} 1699 1700func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1701 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1702} 1703 1704func (p *Pages) Error500(w io.Writer) error { 1705 return p.execute("errors/500", w, nil) 1706} 1707 1708func (p *Pages) Error404(w io.Writer) error { 1709 return p.execute("errors/404", w, nil) 1710} 1711 1712func (p *Pages) ErrorKnot404(w io.Writer) error { 1713 return p.execute("errors/knot404", w, nil) 1714} 1715 1716func (p *Pages) Error503(w io.Writer) error { 1717 return p.execute("errors/503", w, nil) 1718}