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
47 kB 1773 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.TimelineGroup 407 Repos []models.Repo 408 GfiLabel *models.LabelDefinition 409 BlueskyPosts []models.BskyPost 410 VouchSuggestions []models.VouchSuggestion 411 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 412 // For logged-in users it reflects their newsletter_preferences row; for 413 // anonymous visitors it is always true (dismissal falls back to 414 // localStorage on the client). 415 ShowNewsletter bool 416} 417 418func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 419 return p.execute("timeline/timeline", w, params) 420} 421 422type GoodFirstIssuesParams struct { 423 LoggedInUser *oauth.MultiAccountUser 424 Issues []models.Issue 425 RepoGroups []*models.RepoGroup 426 LabelDefs map[string]*models.LabelDefinition 427 GfiLabel *models.LabelDefinition 428 Page pagination.Page 429} 430 431func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 432 return p.execute("goodfirstissues/index", w, params) 433} 434 435type UserProfileSettingsParams struct { 436 LoggedInUser *oauth.MultiAccountUser 437 Tab string 438 PunchcardPreference models.PunchcardPreference 439 IsTnglSh bool 440 IsDeactivated bool 441 HandleOpen bool 442} 443 444func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 445 params.Tab = "profile" 446 return p.execute("user/settings/profile", w, params) 447} 448 449type NotificationsParams struct { 450 LoggedInUser *oauth.MultiAccountUser 451 Notifications []*models.NotificationWithEntity 452 UnreadCount int 453 Page pagination.Page 454 Total int64 455} 456 457func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 458 return p.execute("notifications/list", w, params) 459} 460 461type NotificationItemParams struct { 462 Notification *models.Notification 463} 464 465func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 466 return p.executePlain("notifications/fragments/item", w, params) 467} 468 469type NotificationCountParams struct { 470 Count int64 471} 472 473func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 474 return p.executePlain("notifications/fragments/count", w, params) 475} 476 477type UserKeysSettingsParams struct { 478 LoggedInUser *oauth.MultiAccountUser 479 PubKeys []models.PublicKey 480 Tab string 481} 482 483func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 484 params.Tab = "keys" 485 return p.execute("user/settings/keys", w, params) 486} 487 488type UserEmailsSettingsParams struct { 489 LoggedInUser *oauth.MultiAccountUser 490 Emails []models.Email 491 Tab string 492} 493 494func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 495 params.Tab = "emails" 496 return p.execute("user/settings/emails", w, params) 497} 498 499type UserNotificationSettingsParams struct { 500 LoggedInUser *oauth.MultiAccountUser 501 Preferences *models.NotificationPreferences 502 Tab string 503} 504 505func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 506 params.Tab = "notifications" 507 return p.execute("user/settings/notifications", w, params) 508} 509 510type UserSiteSettingsParams struct { 511 LoggedInUser *oauth.MultiAccountUser 512 Claim *models.DomainClaim 513 SitesDomain string 514 IsTnglHandle bool 515 Tab string 516} 517 518func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 519 params.Tab = "sites" 520 return p.execute("user/settings/sites", w, params) 521} 522 523type UpgradeBannerParams struct { 524 Registrations []models.Registration 525 Spindles []models.Spindle 526} 527 528func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 529 return p.executePlain("banner", w, params) 530} 531 532type NewsletterResponseParams struct { 533 // Id identifies the calling form instance; the response span's id will 534 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target. 535 Id string 536 // Error, when non-empty, switches the template to the error variant. 537 Error string 538} 539 540func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error { 541 return p.executePlain("timeline/fragments/newsletterResponse", w, params) 542} 543 544type KnotsParams struct { 545 LoggedInUser *oauth.MultiAccountUser 546 Registrations []models.Registration 547 Tab string 548} 549 550func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 551 params.Tab = "knots" 552 return p.execute("knots/index", w, params) 553} 554 555type KnotParams struct { 556 LoggedInUser *oauth.MultiAccountUser 557 Registration *models.Registration 558 Members []string 559 Repos map[string][]models.Repo 560 IsOwner bool 561 Tab string 562} 563 564func (p *Pages) Knot(w io.Writer, params KnotParams) error { 565 return p.execute("knots/dashboard", w, params) 566} 567 568type KnotListingParams struct { 569 *models.Registration 570} 571 572func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 573 return p.executePlain("knots/fragments/knotListing", w, params) 574} 575 576type SpindlesParams struct { 577 LoggedInUser *oauth.MultiAccountUser 578 Spindles []models.Spindle 579 Tab string 580} 581 582func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 583 params.Tab = "spindles" 584 return p.execute("spindles/index", w, params) 585} 586 587type SpindleListingParams struct { 588 models.Spindle 589 Tab string 590} 591 592func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 593 return p.executePlain("spindles/fragments/spindleListing", w, params) 594} 595 596type SpindleDashboardParams struct { 597 LoggedInUser *oauth.MultiAccountUser 598 Spindle models.Spindle 599 Members []string 600 Repos map[string][]models.Repo 601 Tab string 602} 603 604func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 605 return p.execute("spindles/dashboard", w, params) 606} 607 608type NewRepoParams struct { 609 LoggedInUser *oauth.MultiAccountUser 610 Knots []string 611} 612 613func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 614 return p.execute("repo/new", w, params) 615} 616 617type ForkRepoParams struct { 618 LoggedInUser *oauth.MultiAccountUser 619 Knots []string 620 RepoInfo repoinfo.RepoInfo 621} 622 623func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 624 return p.execute("repo/fork", w, params) 625} 626 627type ProfileCard struct { 628 UserDid string 629 HasProfile bool 630 FollowStatus models.FollowStatus 631 VouchRelationship *models.VouchRelationship 632 Punchcard *models.Punchcard 633 Profile *models.Profile 634 Stats ProfileStats 635 Active string 636} 637 638type ProfileStats struct { 639 RepoCount int64 640 StarredCount int64 641 StringCount int64 642 FollowersCount int64 643 FollowingCount int64 644} 645 646func (p *ProfileCard) GetTabs() [][]any { 647 tabs := [][]any{ 648 {"overview", "overview", "square-chart-gantt", nil}, 649 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 650 {"starred", "starred", "star", p.Stats.StarredCount}, 651 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 652 {"vouches", "vouches", "shield", nil}, 653 } 654 655 return tabs 656} 657 658type ProfileOverviewParams struct { 659 LoggedInUser *oauth.MultiAccountUser 660 Repos []models.Repo 661 CollaboratingRepos []models.Repo 662 ProfileTimeline *models.ProfileTimeline 663 Card *ProfileCard 664 Active string 665 ShowPunchcard bool 666} 667 668func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 669 params.Active = "overview" 670 return p.executeProfile("user/overview", w, params) 671} 672 673type ProfileReposParams struct { 674 LoggedInUser *oauth.MultiAccountUser 675 Repos []models.Repo 676 Card *ProfileCard 677 Active string 678 Page pagination.Page 679 RepoCount int 680 FilterQuery string 681} 682 683func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 684 params.Active = "repos" 685 return p.executeProfile("user/repos", w, params) 686} 687 688type ProfileStarredParams struct { 689 LoggedInUser *oauth.MultiAccountUser 690 Repos []models.Repo 691 Card *ProfileCard 692 Page pagination.Page 693 Total int 694 Active string 695} 696 697func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 698 params.Active = "starred" 699 return p.executeProfile("user/starred", w, params) 700} 701 702type ProfileStringsParams struct { 703 LoggedInUser *oauth.MultiAccountUser 704 Strings []models.String 705 Card *ProfileCard 706 Active string 707} 708 709func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 710 params.Active = "strings" 711 return p.executeProfile("user/strings", w, params) 712} 713 714type ProfileVouchesParams struct { 715 LoggedInUser *oauth.MultiAccountUser 716 Vouches []models.Vouch 717 Suggestions []models.VouchSuggestion 718 Card *ProfileCard 719 Page pagination.Page 720 Active string 721 EvidencePulls map[syntax.ATURI]*models.Pull 722 EvidenceIssues map[syntax.ATURI]*models.Issue 723} 724 725func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 726 params.Active = "vouches" 727 return p.executeProfile("user/vouches", w, params) 728} 729 730type FollowCard struct { 731 UserDid string 732 LoggedInUser *oauth.MultiAccountUser 733 FollowStatus models.FollowStatus 734 FollowersCount int64 735 FollowingCount int64 736 Profile *models.Profile 737} 738 739type ProfileFollowersParams struct { 740 LoggedInUser *oauth.MultiAccountUser 741 Followers []FollowCard 742 Card *ProfileCard 743 Active string 744} 745 746func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 747 params.Active = "overview" 748 return p.executeProfile("user/followers", w, params) 749} 750 751type ProfileFollowingParams struct { 752 LoggedInUser *oauth.MultiAccountUser 753 Following []FollowCard 754 Card *ProfileCard 755 Active string 756} 757 758func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 759 params.Active = "overview" 760 return p.executeProfile("user/following", w, params) 761} 762 763type FollowFragmentParams struct { 764 UserDid string 765 FollowStatus models.FollowStatus 766 FollowersCount int64 767} 768 769func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 770 return p.executePlain("user/fragments/follow-oob", w, params) 771} 772 773type ProfilePopoverParams struct { 774 LoggedInUser *oauth.MultiAccountUser 775 UserDid string 776 Profile *models.Profile 777 FollowStatus models.FollowStatus 778 VouchRelationship *models.VouchRelationship 779 Stats ProfilePopoverStats 780} 781 782type ProfilePopoverStats struct { 783 FollowersCount int64 784 FollowingCount int64 785} 786 787func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error { 788 return p.executePlain("user/fragments/profilePopover", w, params) 789} 790 791type EditBioParams struct { 792 LoggedInUser *oauth.MultiAccountUser 793 Profile *models.Profile 794 AlsoKnownAs []string 795} 796 797func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 798 return p.executePlain("user/fragments/editBio", w, params) 799} 800 801type EditPinsParams struct { 802 LoggedInUser *oauth.MultiAccountUser 803 Profile *models.Profile 804 AllRepos []PinnedRepo 805} 806 807type PinnedRepo struct { 808 IsPinned bool 809 models.Repo 810} 811 812func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 813 return p.executePlain("user/fragments/editPins", w, params) 814} 815 816type StarBtnFragmentParams struct { 817 IsStarred bool 818 SubjectAt syntax.ATURI 819 StarCount int 820 RepoName string 821 HxSwapOob bool 822} 823 824func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 825 params.HxSwapOob = true 826 return p.executePlain("fragments/starBtn", w, params) 827} 828 829type RepoIndexParams struct { 830 LoggedInUser *oauth.MultiAccountUser 831 RepoInfo repoinfo.RepoInfo 832 Active string 833 TagMap map[string][]string 834 CommitsTrunc []types.Commit 835 TagsTrunc []*types.TagReference 836 BranchesTrunc []types.Branch 837 // ForkInfo *types.ForkInfo 838 HTMLReadme template.HTML 839 Raw bool 840 EmailToDid map[string]string 841 VerifiedCommits commitverify.VerifiedCommits 842 Languages []types.RepoLanguageDetails 843 Pipelines map[string]models.Pipeline 844 NeedsKnotUpgrade bool 845 KnotUnreachable bool 846 types.RepoIndexResponse 847} 848 849func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 850 params.Active = "overview" 851 if params.IsEmpty { 852 return p.executeRepo("repo/empty", w, params) 853 } 854 855 if params.NeedsKnotUpgrade { 856 return p.executeRepo("repo/needsUpgrade", w, params) 857 } 858 859 if params.KnotUnreachable { 860 return p.executeRepo("repo/knotUnreachable", w, params) 861 } 862 863 rctx := p.rctx.Clone() 864 rctx.RepoInfo = params.RepoInfo 865 rctx.RepoInfo.Ref = params.Ref 866 rctx.RendererType = markup.RendererTypeRepoMarkdown 867 868 if params.ReadmeFileName != "" { 869 ext := strings.ToLower(filepath.Ext(params.ReadmeFileName)) 870 switch ext { 871 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 872 params.Raw = false 873 htmlString := rctx.RenderMarkdown(params.Readme) 874 sanitized := rctx.SanitizeDefault(htmlString) 875 params.HTMLReadme = template.HTML(sanitized) 876 default: 877 params.Raw = true 878 } 879 } 880 881 return p.executeRepo("repo/index", w, params) 882} 883 884type RepoLogParams struct { 885 LoggedInUser *oauth.MultiAccountUser 886 RepoInfo repoinfo.RepoInfo 887 TagMap map[string][]string 888 Active string 889 EmailToDid map[string]string 890 VerifiedCommits commitverify.VerifiedCommits 891 Pipelines map[string]models.Pipeline 892 893 types.RepoLogResponse 894} 895 896func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 897 params.Active = "overview" 898 return p.executeRepo("repo/log", w, params) 899} 900 901type RepoCommitParams struct { 902 LoggedInUser *oauth.MultiAccountUser 903 RepoInfo repoinfo.RepoInfo 904 Active string 905 EmailToDid map[string]string 906 Pipeline *models.Pipeline 907 DiffOpts types.DiffOpts 908 909 // singular because it's always going to be just one 910 VerifiedCommit commitverify.VerifiedCommits 911 912 types.RepoCommitResponse 913} 914 915func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 916 params.Active = "overview" 917 return p.executeRepo("repo/commit", w, params) 918} 919 920type RepoTreeParams struct { 921 LoggedInUser *oauth.MultiAccountUser 922 RepoInfo repoinfo.RepoInfo 923 Active string 924 BreadCrumbs [][]string 925 Path string 926 Raw bool 927 HTMLReadme template.HTML 928 EmailToDid map[string]string 929 LastCommitInfo *types.LastCommitInfo 930 types.RepoTreeResponse 931} 932 933type RepoTreeStats struct { 934 NumFolders uint64 935 NumFiles uint64 936} 937 938func (r RepoTreeParams) TreeStats() RepoTreeStats { 939 numFolders, numFiles := 0, 0 940 for _, f := range r.Files { 941 if !f.IsFile() { 942 numFolders += 1 943 } else if f.IsFile() { 944 numFiles += 1 945 } 946 } 947 948 return RepoTreeStats{ 949 NumFolders: uint64(numFolders), 950 NumFiles: uint64(numFiles), 951 } 952} 953 954func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 955 params.Active = "overview" 956 957 rctx := p.rctx.Clone() 958 rctx.RepoInfo = params.RepoInfo 959 rctx.RepoInfo.Ref = params.Ref 960 rctx.RendererType = markup.RendererTypeRepoMarkdown 961 962 if params.ReadmeFileName != "" { 963 ext := strings.ToLower(filepath.Ext(params.ReadmeFileName)) 964 switch ext { 965 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 966 params.Raw = false 967 htmlString := rctx.RenderMarkdown(params.Readme) 968 sanitized := rctx.SanitizeDefault(htmlString) 969 params.HTMLReadme = template.HTML(sanitized) 970 default: 971 params.Raw = true 972 } 973 } 974 975 return p.executeRepo("repo/tree", w, params) 976} 977 978type RepoBranchesParams struct { 979 LoggedInUser *oauth.MultiAccountUser 980 RepoInfo repoinfo.RepoInfo 981 Active string 982 types.RepoBranchesResponse 983} 984 985func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 986 params.Active = "overview" 987 return p.executeRepo("repo/branches", w, params) 988} 989 990type RepoTagsParams struct { 991 LoggedInUser *oauth.MultiAccountUser 992 RepoInfo repoinfo.RepoInfo 993 Active string 994 types.RepoTagsResponse 995 ArtifactMap map[plumbing.Hash][]models.Artifact 996 DanglingArtifacts []models.Artifact 997} 998 999func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1000 params.Active = "overview" 1001 return p.executeRepo("repo/tags", w, params) 1002} 1003 1004type RepoTagParams struct { 1005 LoggedInUser *oauth.MultiAccountUser 1006 RepoInfo repoinfo.RepoInfo 1007 Active string 1008 types.RepoTagResponse 1009 ArtifactMap map[plumbing.Hash][]models.Artifact 1010 DanglingArtifacts []models.Artifact 1011} 1012 1013func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1014 params.Active = "overview" 1015 return p.executeRepo("repo/tag", w, params) 1016} 1017 1018type RepoArtifactParams struct { 1019 LoggedInUser *oauth.MultiAccountUser 1020 RepoInfo repoinfo.RepoInfo 1021 Artifact models.Artifact 1022} 1023 1024func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1025 return p.executePlain("repo/fragments/artifact", w, params) 1026} 1027 1028type RepoBlobParams struct { 1029 LoggedInUser *oauth.MultiAccountUser 1030 RepoInfo repoinfo.RepoInfo 1031 Active string 1032 BreadCrumbs [][]string 1033 BlobView models.BlobView 1034 EmailToDid map[string]string 1035 LastCommitInfo *types.LastCommitInfo 1036 *tangled.RepoBlob_Output 1037} 1038 1039func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1040 params.Active = "overview" 1041 return p.executeRepo("repo/blob", w, params) 1042} 1043 1044type Collaborator struct { 1045 Did string 1046 Role string 1047} 1048 1049type RepoSettingsParams struct { 1050 LoggedInUser *oauth.MultiAccountUser 1051 RepoInfo repoinfo.RepoInfo 1052 Collaborators []Collaborator 1053 Active string 1054 Branches []types.Branch 1055 Spindles []string 1056 CurrentSpindle string 1057 Secrets []*tangled.RepoListSecrets_Secret 1058 1059 // TODO: use repoinfo.roles 1060 IsCollaboratorInviteAllowed bool 1061} 1062 1063func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1064 params.Active = "settings" 1065 return p.executeRepo("repo/settings", w, params) 1066} 1067 1068type RepoGeneralSettingsParams struct { 1069 LoggedInUser *oauth.MultiAccountUser 1070 RepoInfo repoinfo.RepoInfo 1071 Labels []models.LabelDefinition 1072 DefaultLabels []models.LabelDefinition 1073 SubscribedLabels map[string]struct{} 1074 ShouldSubscribeAll bool 1075 Active string 1076 Tab string 1077 Branches []types.Branch 1078} 1079 1080func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1081 params.Active = "settings" 1082 params.Tab = "general" 1083 return p.executeRepo("repo/settings/general", w, params) 1084} 1085 1086type RepoAccessSettingsParams struct { 1087 LoggedInUser *oauth.MultiAccountUser 1088 RepoInfo repoinfo.RepoInfo 1089 Active string 1090 Tab string 1091 Collaborators []Collaborator 1092} 1093 1094func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1095 params.Active = "settings" 1096 params.Tab = "access" 1097 return p.executeRepo("repo/settings/access", w, params) 1098} 1099 1100type RepoPipelineSettingsParams struct { 1101 LoggedInUser *oauth.MultiAccountUser 1102 RepoInfo repoinfo.RepoInfo 1103 Active string 1104 Tab string 1105 Spindles []string 1106 CurrentSpindle string 1107 Secrets []map[string]any 1108} 1109 1110func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1111 params.Active = "settings" 1112 params.Tab = "pipelines" 1113 return p.executeRepo("repo/settings/pipelines", w, params) 1114} 1115 1116type RepoWebhooksSettingsParams struct { 1117 LoggedInUser *oauth.MultiAccountUser 1118 RepoInfo repoinfo.RepoInfo 1119 Active string 1120 Tab string 1121 Webhooks []models.Webhook 1122 WebhookDeliveries map[int64][]models.WebhookDelivery 1123} 1124 1125func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1126 params.Active = "settings" 1127 params.Tab = "hooks" 1128 return p.executeRepo("repo/settings/hooks", w, params) 1129} 1130 1131type WebhookDeliveriesListParams struct { 1132 LoggedInUser *oauth.MultiAccountUser 1133 RepoInfo repoinfo.RepoInfo 1134 Webhook *models.Webhook 1135 Deliveries []models.WebhookDelivery 1136} 1137 1138func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1139 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1140 if err != nil { 1141 return err 1142 } 1143 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1144} 1145 1146type RepoSiteSettingsParams struct { 1147 LoggedInUser *oauth.MultiAccountUser 1148 RepoInfo repoinfo.RepoInfo 1149 Active string 1150 Tab string 1151 Branches []types.Branch 1152 SiteConfig *models.RepoSite 1153 OwnerClaim *models.DomainClaim 1154 Deploys []models.SiteDeploy 1155 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1156} 1157 1158func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1159 params.Active = "settings" 1160 params.Tab = "sites" 1161 return p.executeRepo("repo/settings/sites", w, params) 1162} 1163 1164type RepoIssuesParams struct { 1165 LoggedInUser *oauth.MultiAccountUser 1166 RepoInfo repoinfo.RepoInfo 1167 Active string 1168 Issues []models.Issue 1169 IssueCount int 1170 LabelDefs map[string]*models.LabelDefinition 1171 Page pagination.Page 1172 FilterState string 1173 FilterQuery string 1174 VouchRelationships map[syntax.DID]*models.VouchRelationship 1175} 1176 1177func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1178 params.Active = "issues" 1179 return p.executeRepo("repo/issues/issues", w, params) 1180} 1181 1182type RepoSingleIssueParams struct { 1183 LoggedInUser *oauth.MultiAccountUser 1184 RepoInfo repoinfo.RepoInfo 1185 Active string 1186 Issue *models.Issue 1187 CommentList []models.CommentListItem 1188 Backlinks []models.RichReferenceLink 1189 LabelDefs map[string]*models.LabelDefinition 1190 1191 Reactions map[models.ReactionKind]models.ReactionDisplayData 1192 UserReacted map[models.ReactionKind]bool 1193 VouchRelationships map[syntax.DID]*models.VouchRelationship 1194} 1195 1196func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1197 params.Active = "issues" 1198 return p.executeRepo("repo/issues/issue", w, params) 1199} 1200 1201type EditIssueParams struct { 1202 LoggedInUser *oauth.MultiAccountUser 1203 RepoInfo repoinfo.RepoInfo 1204 Issue *models.Issue 1205 Action string 1206} 1207 1208func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1209 params.Action = "edit" 1210 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1211} 1212 1213type ThreadReactionFragmentParams struct { 1214 ThreadAt syntax.ATURI 1215 Kind models.ReactionKind 1216 Count int 1217 Users []string 1218 IsReacted bool 1219} 1220 1221func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1222 return p.executePlain("repo/fragments/reaction", w, params) 1223} 1224 1225type RepoNewIssueParams struct { 1226 LoggedInUser *oauth.MultiAccountUser 1227 RepoInfo repoinfo.RepoInfo 1228 Issue *models.Issue // existing issue if any -- passed when editing 1229 Active string 1230 Action string 1231} 1232 1233func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1234 params.Active = "issues" 1235 params.Action = "create" 1236 return p.executeRepo("repo/issues/new", w, params) 1237} 1238 1239type EditIssueCommentParams struct { 1240 LoggedInUser *oauth.MultiAccountUser 1241 RepoInfo repoinfo.RepoInfo 1242 Issue *models.Issue 1243 Comment *models.IssueComment 1244} 1245 1246func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1247 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1248} 1249 1250type ReplyIssueCommentPlaceholderParams struct { 1251 LoggedInUser *oauth.MultiAccountUser 1252 RepoInfo repoinfo.RepoInfo 1253 Issue *models.Issue 1254 Comment *models.IssueComment 1255} 1256 1257func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1258 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1259} 1260 1261type ReplyIssueCommentParams struct { 1262 LoggedInUser *oauth.MultiAccountUser 1263 RepoInfo repoinfo.RepoInfo 1264 Issue *models.Issue 1265 Comment *models.IssueComment 1266} 1267 1268func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1269 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1270} 1271 1272type IssueCommentBodyParams struct { 1273 LoggedInUser *oauth.MultiAccountUser 1274 RepoInfo repoinfo.RepoInfo 1275 Issue *models.Issue 1276 Comment *models.IssueComment 1277} 1278 1279func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1280 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1281} 1282 1283type StackedDiff struct { 1284 Diff *types.NiceDiff 1285 Opts types.DiffOpts 1286} 1287 1288type RepoNewPullParams struct { 1289 LoggedInUser *oauth.MultiAccountUser 1290 RepoInfo repoinfo.RepoInfo 1291 Branches []types.Branch 1292 SourceBranches []types.Branch 1293 ForkBranches []types.Branch 1294 Forks []models.Repo 1295 Source Source 1296 SourceBranch string 1297 TargetBranch string 1298 Fork string 1299 Patch string 1300 Title string 1301 Body string 1302 IsStacked bool 1303 Comparison *types.RepoFormatPatchResponse 1304 Diff *types.NiceDiff 1305 DiffOpts types.DiffOpts 1306 StackedDiffs []StackedDiff 1307 MergeCheck *types.MergeCheckResponse 1308 StackTitles map[string]string 1309 StackBodies map[string]string 1310 PrefillError string 1311 Active string 1312 LabelDefs map[string]*models.LabelDefinition 1313 LabelState models.LabelState 1314 StackLabelStates map[string]models.LabelState 1315} 1316 1317func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1318 params.Active = "pulls" 1319 return p.executeRepo("repo/pulls/new", w, params) 1320} 1321 1322func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1323 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1324} 1325 1326func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1327 return p.executePlain("fragments/markdownPreview", w, body) 1328} 1329 1330type RepoPullsParams struct { 1331 LoggedInUser *oauth.MultiAccountUser 1332 RepoInfo repoinfo.RepoInfo 1333 Pulls []*models.Pull 1334 Active string 1335 FilterState string 1336 FilterQuery string 1337 Stacks []models.Stack 1338 Pipelines map[string]models.Pipeline 1339 LabelDefs map[string]*models.LabelDefinition 1340 Page pagination.Page 1341 PullCount int 1342 VouchRelationships map[syntax.DID]*models.VouchRelationship 1343} 1344 1345func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1346 params.Active = "pulls" 1347 return p.executeRepo("repo/pulls/pulls", w, params) 1348} 1349 1350type ResubmitResult uint64 1351 1352const ( 1353 ShouldResubmit ResubmitResult = iota 1354 ShouldNotResubmit 1355 Unknown 1356) 1357 1358func (r ResubmitResult) Yes() bool { 1359 return r == ShouldResubmit 1360} 1361func (r ResubmitResult) No() bool { 1362 return r == ShouldNotResubmit 1363} 1364func (r ResubmitResult) Unknown() bool { 1365 return r == Unknown 1366} 1367 1368type RepoSinglePullParams struct { 1369 LoggedInUser *oauth.MultiAccountUser 1370 RepoInfo repoinfo.RepoInfo 1371 Active string 1372 Pull *models.Pull 1373 Stack models.Stack 1374 Backlinks []models.RichReferenceLink 1375 BranchDeleteStatus *models.BranchDeleteStatus 1376 MergeCheck types.MergeCheckResponse 1377 ResubmitCheck ResubmitResult 1378 Pipelines map[string]models.Pipeline 1379 Diff types.DiffRenderer 1380 DiffOpts types.DiffOpts 1381 ActiveRound int 1382 IsInterdiff bool 1383 1384 Reactions map[models.ReactionKind]models.ReactionDisplayData 1385 UserReacted map[models.ReactionKind]bool 1386 1387 LabelDefs map[string]*models.LabelDefinition 1388 VouchRelationships map[syntax.DID]*models.VouchRelationship 1389 VouchSkips map[syntax.DID]bool 1390} 1391 1392func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1393 params.Active = "pulls" 1394 return p.executeRepo("repo/pulls/pull", w, params) 1395} 1396 1397type RepoPullPatchParams struct { 1398 LoggedInUser *oauth.MultiAccountUser 1399 RepoInfo repoinfo.RepoInfo 1400 Pull *models.Pull 1401 Stack models.Stack 1402 Diff *types.NiceDiff 1403 Round int 1404 Submission *models.PullSubmission 1405 DiffOpts types.DiffOpts 1406} 1407 1408// this name is a mouthful 1409func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1410 return p.execute("repo/pulls/patch", w, params) 1411} 1412 1413type RepoPullInterdiffParams struct { 1414 LoggedInUser *oauth.MultiAccountUser 1415 RepoInfo repoinfo.RepoInfo 1416 Pull *models.Pull 1417 Round int 1418 Interdiff *patchutil.InterdiffResult 1419 DiffOpts types.DiffOpts 1420} 1421 1422// this name is a mouthful 1423func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1424 return p.execute("repo/pulls/interdiff", w, params) 1425} 1426 1427type PullResubmitParams struct { 1428 LoggedInUser *oauth.MultiAccountUser 1429 RepoInfo repoinfo.RepoInfo 1430 Pull *models.Pull 1431 SubmissionId int 1432} 1433 1434func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1435 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1436} 1437 1438type PullActionsParams struct { 1439 LoggedInUser *oauth.MultiAccountUser 1440 RepoInfo repoinfo.RepoInfo 1441 Pull *models.Pull 1442 RoundNumber int 1443 MergeCheck types.MergeCheckResponse 1444 ResubmitCheck ResubmitResult 1445 BranchDeleteStatus *models.BranchDeleteStatus 1446 Stack models.Stack 1447} 1448 1449func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1450 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1451} 1452 1453type PullNewCommentParams struct { 1454 LoggedInUser *oauth.MultiAccountUser 1455 RepoInfo repoinfo.RepoInfo 1456 Pull *models.Pull 1457 RoundNumber int 1458} 1459 1460func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1461 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1462} 1463 1464type RepoCompareParams struct { 1465 LoggedInUser *oauth.MultiAccountUser 1466 RepoInfo repoinfo.RepoInfo 1467 Forks []models.Repo 1468 Branches []types.Branch 1469 Tags []*types.TagReference 1470 Base string 1471 Head string 1472 Diff *types.NiceDiff 1473 DiffOpts types.DiffOpts 1474 1475 Active string 1476} 1477 1478func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1479 params.Active = "overview" 1480 return p.executeRepo("repo/compare/compare", w, params) 1481} 1482 1483type RepoCompareNewParams struct { 1484 LoggedInUser *oauth.MultiAccountUser 1485 RepoInfo repoinfo.RepoInfo 1486 Forks []models.Repo 1487 Branches []types.Branch 1488 Tags []*types.TagReference 1489 Base string 1490 Head string 1491 1492 Active string 1493} 1494 1495func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1496 params.Active = "overview" 1497 return p.executeRepo("repo/compare/new", w, params) 1498} 1499 1500type RepoCompareAllowPullParams struct { 1501 LoggedInUser *oauth.MultiAccountUser 1502 RepoInfo repoinfo.RepoInfo 1503 Base string 1504 Head string 1505} 1506 1507func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1508 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1509} 1510 1511type RepoCompareDiffFragmentParams struct { 1512 Diff types.NiceDiff 1513 DiffOpts types.DiffOpts 1514} 1515 1516func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1517 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1518} 1519 1520type LabelPanelParams struct { 1521 LoggedInUser *oauth.MultiAccountUser 1522 RepoInfo repoinfo.RepoInfo 1523 Defs map[string]*models.LabelDefinition 1524 Subject string 1525 State models.LabelState 1526} 1527 1528func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1529 return p.executePlain("repo/fragments/labelPanel", w, params) 1530} 1531 1532type EditLabelPanelParams struct { 1533 LoggedInUser *oauth.MultiAccountUser 1534 RepoInfo repoinfo.RepoInfo 1535 Defs map[string]*models.LabelDefinition 1536 Subject string 1537 State models.LabelState 1538 Prefix string 1539} 1540 1541func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1542 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1543} 1544 1545type RepoStarsParams struct { 1546 LoggedInUser *oauth.MultiAccountUser 1547 RepoInfo repoinfo.RepoInfo 1548 Active string 1549 Starrers []models.Star 1550 Page pagination.Page 1551 TotalCount int 1552} 1553 1554func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1555 params.Active = "overview" 1556 return p.executeRepo("repo/stars", w, params) 1557} 1558 1559type PipelinesParams struct { 1560 LoggedInUser *oauth.MultiAccountUser 1561 RepoInfo repoinfo.RepoInfo 1562 Pipelines []models.Pipeline 1563 Active string 1564 FilterKind string 1565 Total int64 1566} 1567 1568func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1569 params.Active = "pipelines" 1570 return p.executeRepo("repo/pipelines/pipelines", w, params) 1571} 1572 1573type LogBlockParams struct { 1574 Id int 1575 Name string 1576 Command string 1577 Collapsed bool 1578 StartTime time.Time 1579} 1580 1581func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1582 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1583} 1584 1585type LogBlockEndParams struct { 1586 Id int 1587 StartTime time.Time 1588 EndTime time.Time 1589} 1590 1591func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1592 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1593} 1594 1595type LogLineParams struct { 1596 Id int 1597 Content string 1598} 1599 1600func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1601 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1602} 1603 1604type WorkflowParams struct { 1605 LoggedInUser *oauth.MultiAccountUser 1606 RepoInfo repoinfo.RepoInfo 1607 Pipeline models.Pipeline 1608 Workflow string 1609 LogUrl string 1610 Active string 1611} 1612 1613func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1614 params.Active = "pipelines" 1615 return p.executeRepo("repo/pipelines/workflow", w, params) 1616} 1617 1618type PutStringParams struct { 1619 LoggedInUser *oauth.MultiAccountUser 1620 Action string 1621 1622 // this is supplied in the case of editing an existing string 1623 String models.String 1624} 1625 1626func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1627 return p.execute("strings/put", w, params) 1628} 1629 1630type StringsDashboardParams struct { 1631 LoggedInUser *oauth.MultiAccountUser 1632 Card ProfileCard 1633 Strings []models.String 1634} 1635 1636func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1637 return p.execute("strings/dashboard", w, params) 1638} 1639 1640type StringTimelineParams struct { 1641 LoggedInUser *oauth.MultiAccountUser 1642 Strings []models.String 1643} 1644 1645func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1646 return p.execute("strings/timeline", w, params) 1647} 1648 1649type SingleStringParams struct { 1650 LoggedInUser *oauth.MultiAccountUser 1651 ShowRendered bool 1652 RenderToggle bool 1653 RenderedContents template.HTML 1654 String *models.String 1655 Stats models.StringStats 1656 IsStarred bool 1657 StarCount int 1658 Owner identity.Identity 1659} 1660 1661func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1662 return p.execute("strings/string", w, params) 1663} 1664 1665type SearchReposParams struct { 1666 LoggedInUser *oauth.MultiAccountUser 1667 Repos []models.Repo 1668 Page pagination.Page 1669 ResultCount int 1670 FilterQuery string 1671 SortParam string 1672 TimeTaken time.Duration 1673 DocCount int64 1674} 1675 1676func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1677 return p.execute("search/search", w, params) 1678} 1679 1680type SearchQuickParams struct { 1681 Repos []models.Repo 1682 Query string 1683 Total int 1684} 1685 1686func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1687 return p.executePlain("search/fragments/quick", w, params) 1688} 1689 1690func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1691 tpl, err := p.parse("search/fragments/quick") 1692 if err != nil { 1693 return err 1694 } 1695 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1696} 1697 1698func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1699 return p.execute("timeline/home", w, params) 1700} 1701 1702func (p *Pages) Static() http.Handler { 1703 if p.dev { 1704 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1705 } 1706 1707 sub, err := fs.Sub(p.embedFS, "static") 1708 if err != nil { 1709 p.logger.Error("no static dir found? that's crazy", "err", err) 1710 panic(err) 1711 } 1712 // Custom handler to apply Cache-Control headers for font files 1713 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1714} 1715 1716func Cache(h http.Handler) http.Handler { 1717 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1718 path := strings.Split(r.URL.Path, "?")[0] 1719 1720 if strings.HasSuffix(path, ".css") { 1721 // on day for css files 1722 w.Header().Set("Cache-Control", "public, max-age=86400") 1723 } else { 1724 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1725 } 1726 h.ServeHTTP(w, r) 1727 }) 1728} 1729 1730func (p *Pages) CssContentHash() string { 1731 cssFile, err := p.embedFS.Open("static/tw.css") 1732 if err != nil { 1733 slog.Debug("Error opening CSS file", "err", err) 1734 return "" 1735 } 1736 defer cssFile.Close() 1737 1738 hasher := sha256.New() 1739 if _, err := io.Copy(hasher, cssFile); err != nil { 1740 slog.Debug("Error hashing CSS file", "err", err) 1741 return "" 1742 } 1743 1744 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1745} 1746 1747func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1748 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1749} 1750 1751func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1752 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1753} 1754 1755func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1756 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1757} 1758 1759func (p *Pages) Error500(w io.Writer) error { 1760 return p.execute("errors/500", w, nil) 1761} 1762 1763func (p *Pages) Error404(w io.Writer) error { 1764 return p.execute("errors/404", w, nil) 1765} 1766 1767func (p *Pages) ErrorKnot404(w io.Writer) error { 1768 return p.execute("errors/knot404", w, nil) 1769} 1770 1771func (p *Pages) Error503(w io.Writer) error { 1772 return p.execute("errors/503", w, nil) 1773}