slint::slint! { import { Button, ComboBox, LineEdit, ListView, ScrollView, Spinner } from "std-widgets.slint"; export struct ServerRow { name: string, url: string } export struct RepositoryRow { name: string, owner: string, description: string, meta: string, favorite: bool, } export struct LabelRow { name: string, background: color, foreground: color } export struct IssueRow { number: int, title: string, summary: string, meta: string, labels: [LabelRow], } export struct PullRow { number: int, owner: string, repository: string, title: string, summary: string, meta: string, } export struct MarkdownBlock { text: styled-text, kind: string } export struct CommentRow { author: string, body: [MarkdownBlock], meta: string } export struct GraphLane { color: color, top: bool, bottom: bool, node: bool, connect_left: bool } export struct CommitRow { sha: string, title: string, meta: string, refs: string, lanes: [GraphLane], } export struct FileRow { path: string, status: string } export struct ActivityRow { icon: string, title: string, detail: string, meta: string, target: string, owner: string, repository: string, number: int, sha: string, } export struct DiffLine { old_number: string, new_number: string, text: string, kind: string } export struct HeatCell { week: int, day: int, level: int } component Header inherits Rectangle { in property title; in property can_back; in property can_filter: false; callback back(); callback filter(); height: 58px; width: 100%; background: #f8f8fa; border-color: #dedee3; border-width: 0px; if can_back: TouchArea { x: 8px; width: 54px; clicked => { root.back(); } Text { text: "‹"; color: #0879e1; font-size: 38px; vertical-alignment: center; } } Text { x: root.can_back || root.can_filter ? 62px : 12px; width: parent.width - 2 * self.x; text: root.title; horizontal-alignment: center; vertical-alignment: center; font-size: 18px; font-weight: 600; color: #151518; overflow: elide; } if can_filter: TouchArea { x: parent.width - 58px; width: 58px; clicked => { root.filter(); } Text { text: "☷"; color: #0879e1; font-size: 25px; horizontal-alignment: center; vertical-alignment: center; } } Rectangle { y: parent.height - 1px; height: 1px; background: #dedee3; } } component EdgeBack inherits SwipeGestureHandler { callback back(); width: 24px; height: 100%; handle-swipe-right: true; swiped => { root.back(); } } component EmptyState inherits VerticalLayout { in property title; in property detail; alignment: center; spacing: 8px; Text { text: root.title; font-size: 21px; font-weight: 600; horizontal-alignment: center; color: #1c1c1e; } Text { text: root.detail; font-size: 15px; horizontal-alignment: center; wrap: word-wrap; color: #69696f; } } component MarkdownContent inherits VerticalLayout { in property <[MarkdownBlock]> blocks; spacing: 8px; for block in root.blocks: Rectangle { height: block.kind == "rule" ? 9px : rendered.preferred-height + (block.kind == "code" || block.kind == "table" ? 16px : 0px); background: block.kind == "code" || block.kind == "table" ? #e9e9ed : block.kind == "quote" ? #f6f6f8 : transparent; border-radius: block.kind == "code" || block.kind == "table" ? 7px : 0px; if block.kind == "quote": Rectangle { width: 3px; height: 100%; background: #9a9aa1; border-radius: 2px; } if block.kind == "rule": Rectangle { y: 4px; width: 100%; height: 1px; background: #d2d2d7; } rendered := StyledText { x: block.kind == "code" || block.kind == "table" ? 8px : block.kind == "quote" ? 11px : 0px; y: block.kind == "code" || block.kind == "table" ? 8px : 0px; width: parent.width - self.x - (block.kind == "code" || block.kind == "table" ? 8px : 0px); text: block.text; default-color: #252529; link-color: #0879e1; default-font-family: block.kind == "code" || block.kind == "table" ? "monospace" : ""; default-font-size: block.kind == "heading1" ? 24px : block.kind == "heading2" ? 21px : block.kind == "heading3" ? 19px : block.kind == "heading4" ? 17px : 16px; } } } component CommentCard inherits Rectangle { in property comment; height: content.preferred-height + 24px; background: #ffffff; border-radius: 10px; content := VerticalLayout { x: 12px; y: 10px; width: parent.width - 24px; spacing: 5px; Text { text: root.comment.author; font-size: 13px; font-weight: 600; color: #252529; } MarkdownContent { width: parent.width; blocks: root.comment.body; } Text { text: root.comment.meta; font-size: 11px; color: #85858b; } } } component FileCard inherits Rectangle { in property file; callback clicked(); height: 66px; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 10px; TouchArea { clicked => { root.clicked(); } } Text { x: 13px; y: 10px; width: parent.width - 26px; text: root.file.path; font-size: 14px; font-weight: 600; color: #232327; overflow: elide; } Text { x: 13px; y: 38px; width: parent.width - 26px; text: root.file.status; font-size: 12px; color: #74747a; } } component TabButton inherits Rectangle { in property icon; in property label; in property active; callback clicked(); background: transparent; TouchArea { clicked => { root.clicked(); } } Text { y: 4px; height: 29px; width: 100%; text: root.icon; color: root.active ? #0879e1 : #77777d; font-size: 24px; horizontal-alignment: center; vertical-alignment: center; } Text { y: 34px; height: 18px; width: 100%; text: root.label; color: root.active ? #0879e1 : #77777d; font-size: 10px; font-weight: root.active ? 600 : 400; horizontal-alignment: center; vertical-alignment: center; } } component PullToRefreshGesture inherits SwipeGestureHandler { in property at_top; in property refreshing; callback refresh(); enabled: root.at_top && !root.refreshing; handle-swipe-down: true; swiped => { root.refresh(); } @children if self.swiping: Rectangle { x: (parent.width - 142px) / 2; y: 8px; width: 142px; height: 32px; background: #e9e9ee; border-radius: 16px; Text { text: "Release to refresh"; font-size: 12px; color: #55555b; horizontal-alignment: center; vertical-alignment: center; } } } export component AppWindow inherits Window { title: "Gotcha"; preferred-width: 390px; preferred-height: 844px; background: #f2f2f7; in-out property tab: "home"; in-out property page: "servers"; in-out property <[ServerRow]> servers; in-out property <[RepositoryRow]> repositories; in-out property <[IssueRow]> issues; in-out property <[PullRow]> pulls; in-out property <[CommentRow]> comments; in-out property <[CommitRow]> commits; in-out property <[FileRow]> files; in-out property <[ActivityRow]> activities; in-out property <[HeatCell]> heat_cells; in-out property contribution_count; in-out property has_active_server; in-out property home_server; in-out property server_title; in-out property repository_title; in-out property issue_title; in-out property issue_meta; in-out property <[MarkdownBlock]> issue_body; in-out property detail_title; in-out property detail_meta; in-out property detail_files_ref; in-out property <[MarkdownBlock]> detail_body; in-out property <[DiffLine]> diff_lines; in-out property diff_columns; in-out property issue_filter: "open"; in-out property pull_filter: "open"; in-out property <[string]> branch_choices; in-out property branch_index; in-out property filter_open; in-out property loading; in-out property home_loading; in-out property refreshing; in-out property error; in-out property home_scroll_request; in-out property servers_scroll_request; in-out property repositories_scroll_request; in-out property issues_scroll_request; in-out property issue_scroll_request; private property home_at_top: true; private property repositories_at_top: true; private property issues_at_top: true; private property issue_at_top: true; callback select_tab(string); callback refresh_home(); callback refresh_repositories(); callback refresh_issues(); callback refresh_issue(); callback refresh_pulls(); callback refresh_pull(); callback refresh_commits(); callback show_add_server(); callback save_server(string, string, string); callback select_server(int); callback select_repository(string, string); callback toggle_favorite(string, string); callback select_issue(int); callback select_pull(string, string, int); callback select_commit(string); callback select_branch(string); callback select_file(string); callback select_pull_file(string); callback select_activity(string, string, string, int, string); callback choose_filter(string); callback back(); if tab == "home": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: root.has_active_server ? root.home_server : "Home"; can_back: false; } if !root.has_active_server: EmptyState { x: 32px; width: parent.width - 64px; y: 210px; height: 190px; title: "No server selected"; detail: "Open Issues to select a server or add your first one."; } if root.has_active_server: Rectangle { x: 16px; y: 72px; width: parent.width - 32px; height: 118px; background: #ffffff; border-radius: 12px; Text { x: 13px; y: 10px; text: "Activity · last 12 months"; font-size: 13px; font-weight: 600; color: #38383d; } Text { x: 13px; y: 86px; text: root.contribution_count + " contributions"; font-size: 12px; color: #74747a; } for cell in root.heat_cells: Rectangle { x: 13px + cell.week * 6.7px; y: 39px + cell.day * 6.2px; width: 5.2px; height: 5.2px; border-radius: 1px; background: cell.level == 0 ? #e5e5e9 : cell.level == 1 ? #b8d9f4 : cell.level == 2 ? #72b5e8 : cell.level == 3 ? #278bd4 : #0969b7; } } if root.has_active_server && !root.home_loading && root.activities.length == 0: EmptyState { x: 32px; width: parent.width - 64px; y: 250px; height: 150px; title: "No recent activity"; detail: "Your recent server actions will appear here."; } if root.has_active_server: PullToRefreshGesture { x: 16px; y: 202px; width: parent.width - 32px; height: parent.height - 214px; at_top: root.home_at_top; refreshing: root.home_loading; refresh => { root.refresh_home(); } home_list := ListView { property scroll_request: root.home_scroll_request; width: 100%; height: 100%; mouse-drag-pan-enabled: true; changed scroll_request => { self.viewport-y = 0px; root.home_at_top = true; } scrolled => { root.home_at_top = self.viewport-y >= 0px; } for activity in root.activities: Rectangle { height: 94px; background: #ffffff; border-radius: 12px; TouchArea { enabled: activity.target != ""; clicked => { root.select_activity(activity.target, activity.owner, activity.repository, activity.number, activity.sha); } } Rectangle { x: 12px; y: 14px; width: 34px; height: 34px; border-radius: 17px; background: #e5f2fd; Text { text: activity.icon; color: #0879e1; font-size: 20px; horizontal-alignment: center; vertical-alignment: center; } } Text { x: 57px; y: 11px; width: parent.width - 69px; height: 24px; text: activity.title; font-size: 15px; font-weight: 600; color: #19191d; overflow: elide; } Text { x: 57px; y: 36px; width: parent.width - 69px; height: 35px; text: activity.detail; font-size: 13px; color: #55555c; wrap: word-wrap; overflow: elide; } Text { x: 57px; y: 72px; width: parent.width - 69px; text: activity.meta; font-size: 11px; color: #898990; overflow: elide; } } } } if root.home_loading: Spinner { width: 34px; height: 34px; x: (parent.width - self.width) / 2; y: 222px; } } if tab == "settings": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; can_back: false; title: "Settings"; } EmptyState { x: 32px; width: parent.width - 64px; y: 220px; height: 160px; title: "Coming soon"; detail: "This section is ready for the next feature pass."; } } if (tab == "issues" || tab == "repositories") && page == "servers": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Servers"; can_back: false; } if root.servers.length > 0: servers_list := ListView { property scroll_request: root.servers_scroll_request; x: 16px; y: 76px; width: parent.width - 32px; height: parent.height - 154px; mouse-drag-pan-enabled: true; changed scroll_request => { self.viewport-y = 0px; } for server[index] in root.servers: Rectangle { height: 76px; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_server(index); } } Text { x: 16px; y: 13px; width: parent.width - 54px; text: server.name; font-size: 17px; font-weight: 600; color: #19191c; overflow: elide; } Text { x: 16px; y: 42px; width: parent.width - 54px; text: server.url; font-size: 13px; color: #6c6c72; overflow: elide; } Text { x: parent.width - 28px; text: "›"; font-size: 27px; color: #a0a0a6; vertical-alignment: center; } } } Button { x: 16px; y: parent.height - 66px; width: parent.width - 32px; height: 48px; text: "Add Server"; clicked => { root.show_add_server(); } } } if (tab == "issues" || tab == "repositories") && page == "add": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Add Server"; can_back: true; back => { root.back(); } } VerticalLayout { x: 20px; y: 92px; width: parent.width - 40px; height: 260px; spacing: 10px; Text { text: "Name"; font-size: 13px; color: #66666c; } server_name := LineEdit { placeholder-text: "Work"; } Text { text: "Server URL"; font-size: 13px; color: #66666c; } server_url := LineEdit { placeholder-text: "https://gitea.example.com"; } Text { text: "Access token"; font-size: 13px; color: #66666c; } server_token := LineEdit { placeholder-text: "Token"; input-type: InputType.password; } } Button { x: 20px; y: 380px; width: parent.width - 40px; height: 48px; text: root.loading ? "Connecting…" : "Add Server"; enabled: !root.loading; clicked => { root.save_server(server_name.text, server_url.text, server_token.text); } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if (tab == "issues" || tab == "repositories") && page == "repositories": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: root.server_title; can_back: true; back => { root.back(); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } if !root.loading && root.repositories.length == 0: EmptyState { x: 34px; width: parent.width - 68px; y: 220px; height: 160px; title: "No repositories"; detail: "This account does not own any repositories on this server."; } PullToRefreshGesture { x: 20px; y: 70px; width: parent.width - 40px; height: parent.height - 82px; at_top: root.repositories_at_top; refreshing: root.refreshing; refresh => { root.refresh_repositories(); } repositories_list := ListView { property scroll_request: root.repositories_scroll_request; width: 100%; height: 100%; mouse-drag-pan-enabled: true; changed scroll_request => { self.viewport-y = 0px; root.repositories_at_top = true; } scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } for repo in root.repositories: Rectangle { height: 104px; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_repository(repo.owner, repo.name); } } Text { x: 15px; y: 11px; width: parent.width - 62px; text: repo.name; font-size: 17px; font-weight: 600; color: #17171a; overflow: elide; } Text { x: 15px; y: 39px; width: parent.width - 30px; text: repo.description; font-size: 14px; color: #56565c; overflow: elide; } Text { x: 15px; y: 71px; width: parent.width - 30px; text: repo.meta; font-size: 12px; color: #7b7b82; overflow: elide; } TouchArea { x: parent.width - 52px; y: 2px; width: 48px; height: 48px; clicked => { root.toggle_favorite(repo.owner, repo.name); } Text { text: repo.favorite ? "★" : "☆"; color: repo.favorite ? #f2a900 : #888890; font-size: 27px; horizontal-alignment: center; vertical-alignment: center; } } } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if tab == "issues" && page == "issues": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: root.repository_title; can_back: true; can_filter: true; back => { root.back(); } filter => { root.filter_open = true; } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } if !root.loading && root.issues.length == 0: EmptyState { x: 34px; width: parent.width - 68px; y: 220px; height: 160px; title: "No " + root.issue_filter + " issues"; detail: "No issues match the selected status."; } PullToRefreshGesture { x: 20px; y: 70px; width: parent.width - 40px; height: parent.height - 82px; at_top: root.issues_at_top; refreshing: root.refreshing; refresh => { root.refresh_issues(); } issues_list := ListView { property scroll_request: root.issues_scroll_request; width: 100%; height: 100%; mouse-drag-pan-enabled: true; changed scroll_request => { self.viewport-y = 0px; root.issues_at_top = true; } scrolled => { root.issues_at_top = self.viewport-y >= 0px; } for issue in root.issues: Rectangle { height: issue.labels.length > 0 ? 138px : 112px; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_issue(issue.number); } } Text { x: 15px; y: 11px; width: parent.width - 44px; text: issue.title; font-size: 16px; font-weight: 600; color: #17171a; overflow: elide; } Text { x: 15px; y: 39px; width: parent.width - 30px; height: 38px; text: issue.summary; font-size: 13px; color: #56565c; wrap: word-wrap; overflow: elide; } if issue.labels.length > 0: HorizontalLayout { x: 15px; y: 82px; width: parent.width - 30px; height: 22px; spacing: 5px; for label in issue.labels: Rectangle { width: min(104px, max(48px, (parent.width - (issue.labels.length - 1) * 5px) / issue.labels.length)); height: 22px; background: label.background; border-radius: 11px; Text { x: 7px; width: parent.width - 14px; text: label.name; color: label.foreground; font-size: 11px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; overflow: elide; } } } Text { x: 15px; y: issue.labels.length > 0 ? 112px : 86px; width: parent.width - 30px; text: issue.meta; font-size: 12px; color: #7b7b82; overflow: elide; } } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if tab == "issues" && page == "issue": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Issue"; can_back: true; back => { root.back(); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } PullToRefreshGesture { x: 18px; y: 78px; width: parent.width - 36px; height: parent.height - 96px; at_top: root.issue_at_top; refreshing: root.refreshing; refresh => { root.refresh_issue(); } issue_scroll := ScrollView { property scroll_request: root.issue_scroll_request; width: 100%; height: 100%; mouse-drag-pan-enabled: true; changed scroll_request => { self.viewport-y = 0px; root.issue_at_top = true; } scrolled => { root.issue_at_top = self.viewport-y >= 0px; } VerticalLayout { width: parent.width; spacing: 12px; alignment: start; Text { text: root.issue_title; font-size: 23px; font-weight: 650; color: #161619; wrap: word-wrap; } Text { text: root.issue_meta; font-size: 13px; color: #707077; wrap: word-wrap; } Rectangle { height: 1px; background: #d8d8dd; } MarkdownContent { width: parent.width; blocks: root.issue_body; } if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: #252529; } for comment in root.comments: CommentCard { width: parent.width; comment: comment; } } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if tab == "pulls" && !root.has_active_server: Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Home"; can_back: false; } EmptyState { x: 32px; width: parent.width - 64px; y: 210px; height: 190px; title: "No server selected"; detail: "Open Issues or Repositories to select a server or add your first one."; } } if tab == "pulls" && root.has_active_server && page == "pulls": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Pull Requests"; can_back: false; can_filter: true; filter => { root.filter_open = true; } } if root.refreshing: Spinner { x: parent.width - 92px; y: 17px; width: 24px; height: 24px; } if !root.loading && root.pulls.length == 0: EmptyState { x: 34px; width: parent.width - 68px; y: 220px; height: 160px; title: "No " + root.pull_filter + " pull requests"; detail: "No pull requests match the selected status."; } PullToRefreshGesture { x: 16px; y: 70px; width: parent.width - 32px; height: parent.height - 82px; at_top: root.issues_at_top; refreshing: root.refreshing; refresh => { root.refresh_pulls(); } ListView { width: 100%; height: 100%; mouse-drag-pan-enabled: true; scrolled => { root.issues_at_top = self.viewport-y >= 0px; } for pull in root.pulls: Rectangle { height: 112px; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_pull(pull.owner, pull.repository, pull.number); } } Text { x: 14px; y: 10px; width: parent.width - 28px; text: pull.repository + " #" + pull.number; font-size: 12px; color: #727278; overflow: elide; } Text { x: 14px; y: 31px; width: parent.width - 28px; text: pull.title; font-size: 16px; font-weight: 600; color: #17171a; overflow: elide; } Text { x: 14px; y: 57px; width: parent.width - 28px; text: pull.summary; font-size: 13px; color: #56565c; overflow: elide; } Text { x: 14px; y: 85px; width: parent.width - 28px; text: pull.meta; font-size: 12px; color: #7b7b82; overflow: elide; } } } } } if tab == "pulls" && page == "pull": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Pull Request"; can_back: true; back => { root.back(); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } PullToRefreshGesture { x: 18px; y: 78px; width: parent.width - 36px; height: parent.height - 96px; at_top: root.issue_at_top; refreshing: root.refreshing; refresh => { root.refresh_pull(); } ScrollView { width: 100%; height: 100%; mouse-drag-pan-enabled: true; scrolled => { root.issue_at_top = self.viewport-y >= 0px; } VerticalLayout { width: parent.width; spacing: 12px; alignment: start; Text { text: root.detail_title; font-size: 23px; font-weight: 650; color: #161619; wrap: word-wrap; } Text { text: root.detail_meta; font-size: 13px; color: #707077; wrap: word-wrap; } Rectangle { height: 1px; background: #d8d8dd; } MarkdownContent { width: parent.width; blocks: root.detail_body; } if root.files.length > 0: Text { text: root.detail_files_ref; font-size: 17px; font-weight: 600; color: #252529; } for file in root.files: FileCard { width: parent.width; file: file; clicked => { root.select_pull_file(file.path); } } if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: #252529; } for comment in root.comments: CommentCard { width: parent.width; comment: comment; } } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if tab == "repositories" && page == "commits": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: root.repository_title; can_back: true; back => { root.back(); } } ComboBox { x: 16px; y: 66px; width: parent.width - 32px; height: 42px; model: root.branch_choices; current-index: root.branch_index; selected(value) => { root.select_branch(value); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } if !root.loading && root.commits.length == 0: EmptyState { x: 34px; width: parent.width - 68px; y: 220px; height: 160px; title: "No commits"; detail: "This repository has no commit history."; } PullToRefreshGesture { x: 16px; y: 116px; width: parent.width - 32px; height: parent.height - 128px; at_top: root.repositories_at_top; refreshing: root.refreshing; refresh => { root.refresh_commits(); } ListView { width: 100%; height: 100%; mouse-drag-pan-enabled: true; scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } for commit in root.commits: commit_row := Rectangle { property graph-width: commit.lanes.length > 0 ? min(72px, commit.lanes.length * 10px + 8px) : 0px; property lane-spacing: (self.graph-width - 8px) / max(1, commit.lanes.length); height: 86px; background: transparent; Rectangle { width: 100%; height: 100%; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; } TouchArea { clicked => { root.select_commit(commit.sha); } } for lane[index] in commit.lanes: Rectangle { x: 4px + index * commit_row.lane-spacing; width: 3px; height: 100%; background: transparent; if lane.top: Rectangle { y: -1px; width: 3px; height: lane.connect_left && !lane.bottom ? 37px : 42px; background: lane.color; } if lane.bottom: Rectangle { y: lane.connect_left && !lane.top ? 45px : 40px; width: 3px; height: parent.height - (lane.connect_left && !lane.top ? 44px : 39px); background: lane.color; } if lane.connect_left && lane.top == lane.bottom: Rectangle { x: -commit_row.lane-spacing; y: 39px; width: commit_row.lane-spacing + 3px; height: 3px; background: lane.color; } if lane.connect_left && lane.bottom && !lane.top: Path { x: 1.5px - commit_row.lane-spacing; y: 35px; width: commit_row.lane-spacing; height: 11px; viewbox-width: 10; viewbox-height: 11; commands: "M 0 5.5 L 5 5.5 Q 10 5.5 10 10.5"; fill: transparent; stroke: lane.color; stroke-width: 3px; stroke-line-cap: round; } if lane.connect_left && lane.top && !lane.bottom: Path { x: 1.5px - commit_row.lane-spacing; y: 35px; width: commit_row.lane-spacing; height: 11px; viewbox-width: 10; viewbox-height: 11; commands: "M 10 0.5 Q 10 5.5 5 5.5 L 0 5.5"; fill: transparent; stroke: lane.color; stroke-width: 3px; stroke-line-cap: round; } if lane.node: Rectangle { x: -3px; y: 36px; width: 9px; height: 9px; border-radius: 5px; background: lane.color; border-color: #ffffff; border-width: 2px; } } Text { x: parent.graph-width + 14px; y: 8px; width: parent.width - self.x - 14px; text: commit.title; font-size: 15px; font-weight: 600; color: #17171a; overflow: elide; } Text { x: parent.graph-width + 14px; y: 33px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.refs : commit.meta; font-size: 12px; color: commit.refs != "" ? #0879e1 : #707077; overflow: elide; } Text { x: parent.graph-width + 14px; y: 53px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.meta + " · " + commit.sha : commit.sha; font-size: 11px; color: #919198; overflow: elide; } } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if tab == "repositories" && page == "files": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: "Changed Files"; can_back: true; back => { root.back(); } } if !root.loading && root.files.length == 0: EmptyState { x: 34px; width: parent.width - 68px; y: 220px; height: 160px; title: "No changed files"; detail: "This commit does not contain file changes."; } ListView { x: 16px; y: 70px; width: parent.width - 32px; height: parent.height - 82px; mouse-drag-pan-enabled: true; for file in root.files: FileCard { file: file; clicked => { root.select_file(file.path); } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } if (tab == "repositories" || tab == "pulls") && page == "diff": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - 58px; background: #f2f2f7; Header { x: 0; y: 0; title: root.detail_title; can_back: true; back => { root.back(); } } ScrollView { x: 12px; y: 70px; width: parent.width - 24px; height: parent.height - 82px; mouse-drag-pan-enabled: true; viewport-width: max(self.visible-width, root.diff_columns * 7px + 100px); viewport-height: diff_rows.preferred-height; diff_rows := VerticalLayout { width: parent.viewport-width; for line in root.diff_lines: Rectangle { height: 20px; background: line.kind == "addition" ? #ddf5e3 : line.kind == "removal" ? #fde2e1 : line.kind == "hunk" ? #dcecff : line.kind == "header" ? #ececf0 : #ffffff; Text { x: 0; width: 38px; text: line.old_number; font-family: "monospace"; font-size: 11px; color: #85858c; horizontal-alignment: right; vertical-alignment: center; } Text { x: 42px; width: 38px; text: line.new_number; font-family: "monospace"; font-size: 11px; color: #85858c; horizontal-alignment: right; vertical-alignment: center; } Rectangle { x: 84px; width: 1px; height: 100%; background: #d5d5da; } Text { x: 90px; width: parent.width - 94px; text: line.text; font-family: "monospace"; font-size: 11px; color: line.kind == "addition" ? #176b2c : line.kind == "removal" ? #a12720 : line.kind == "hunk" ? #175b9e : #202024; wrap: no-wrap; overflow: clip; vertical-alignment: center; } } } } EdgeBack { x: 0; y: 0; back => { root.back(); } } } Rectangle { x: root.safe-area-insets.left; y: root.height - root.safe-area-insets.bottom - 58px; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: 58px; background: #fbfbfc; Rectangle { width: 100%; height: 1px; background: #d4d4d8; } TabButton { x: 0; width: parent.width / 5; height: parent.height; icon: "⌂"; label: "Home"; active: root.tab == "home"; clicked => { if root.tab == "home" { root.home_scroll_request += 1; } root.select_tab("home"); } } TabButton { x: parent.width / 5; width: parent.width / 5; height: parent.height; icon: "!"; label: "Issues"; active: root.tab == "issues"; clicked => { if root.tab == "issues" { if root.page == "servers" { root.servers_scroll_request += 1; } if root.page == "repositories" { root.repositories_scroll_request += 1; } if root.page == "issues" { root.issues_scroll_request += 1; } if root.page == "issue" { root.issue_scroll_request += 1; } } root.select_tab("issues"); } } TabButton { x: parent.width * 2 / 5; width: parent.width / 5; height: parent.height; icon: "▱"; label: "Repos"; active: root.tab == "repositories"; clicked => { root.select_tab("repositories"); } } TabButton { x: parent.width * 3 / 5; width: parent.width / 5; height: parent.height; icon: "↗"; label: "Pulls"; active: root.tab == "pulls"; clicked => { root.select_tab("pulls"); } } TabButton { x: parent.width * 4 / 5; width: parent.width / 5; height: parent.height; icon: "≡"; label: "Settings"; active: root.tab == "settings"; clicked => { root.select_tab("settings"); } } } if root.loading && (root.tab == "issues" || root.tab == "repositories" || root.tab == "pulls"): Rectangle { width: 100%; height: 100%; background: #55ffffff; TouchArea { } Spinner { width: 38px; height: 38px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } } if root.filter_open: Rectangle { width: 100%; height: 100%; background: #66000000; TouchArea { clicked => { root.filter_open = false; } } Rectangle { x: 28px; y: (parent.height - 190px) / 2; width: parent.width - 56px; height: 190px; background: #ffffff; border-radius: 14px; Text { x: 18px; y: 16px; width: parent.width - 36px; text: "Status"; font-size: 19px; font-weight: 650; color: #202024; } VerticalLayout { x: 18px; y: 54px; width: parent.width - 36px; height: 118px; spacing: 8px; Button { text: "Open"; clicked => { root.choose_filter("open"); root.filter_open = false; } } Button { text: "Closed"; clicked => { root.choose_filter("closed"); root.filter_open = false; } } } } } if root.error != "": Rectangle { x: 14px; y: parent.height - root.safe-area-insets.bottom - 132px; width: parent.width - 28px; height: 66px; background: #d93d35; border-radius: 12px; Text { x: 14px; width: parent.width - 28px; text: root.error; color: white; font-size: 13px; wrap: word-wrap; vertical-alignment: center; } TouchArea { clicked => { root.error = ""; } } } } }