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