mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Add fullscreen mode as a more efficient operation way to view projects (#34081)
Maybe fix #33482, maybe fix #34015 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -3844,6 +3844,8 @@ deleted.display_name = Deleted Project | |||||||
| type-1.display_name = Individual Project | type-1.display_name = Individual Project | ||||||
| type-2.display_name = Repository Project | type-2.display_name = Repository Project | ||||||
| type-3.display_name = Organization Project | type-3.display_name = Organization Project | ||||||
|  | enter_fullscreen = Fullscreen | ||||||
|  | exit_fullscreen = Exit Fullscreen | ||||||
|  |  | ||||||
| [git.filemode] | [git.filemode] | ||||||
| changed_filemode = %[1]s → %[2]s | changed_filemode = %[1]s → %[2]s | ||||||
|   | |||||||
| @@ -8,8 +8,6 @@ | |||||||
| 			{{template "user/overview/header" .}} | 			{{template "user/overview/header" .}} | ||||||
| 		</div> | 		</div> | ||||||
| 	{{end}} | 	{{end}} | ||||||
| 	<div class="ui container fluid padded"> | 	{{template "projects/view" .}} | ||||||
| 		{{template "projects/view" .}} |  | ||||||
| 	</div> |  | ||||||
| </div> | </div> | ||||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} | {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} | ||||||
|  |  | ||||||
| <div class="ui container tw-max-w-full"> | <div class="ui container fluid padded projects-view"> | ||||||
| 	<div class="flex-text-block tw-flex-wrap tw-mb-4"> | 	<div class="ui container flex-text-block project-header"> | ||||||
| 		<h2 class="tw-mb-0">{{.Project.Title}}</h2> | 		<h2>{{.Project.Title}}</h2> | ||||||
| 		<div class="tw-flex-1"></div> | 		<div class="tw-flex-1"></div> | ||||||
| 		<div class="ui secondary menu tw-m-0"> | 		<div class="ui secondary menu tw-m-0"> | ||||||
| 			{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}} | 			{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}} | ||||||
| @@ -19,6 +19,14 @@ | |||||||
| 		</div> | 		</div> | ||||||
| 		{{if $canWriteProject}} | 		{{if $canWriteProject}} | ||||||
| 			<div class="ui compact mini menu"> | 			<div class="ui compact mini menu"> | ||||||
|  | 				<a class="item screen-full"> | ||||||
|  | 					{{svg "octicon-screen-full"}} | ||||||
|  | 					{{ctx.Locale.Tr "projects.enter_fullscreen"}} | ||||||
|  | 				</a> | ||||||
|  | 				<a class="item screen-normal tw-hidden"> | ||||||
|  | 					{{svg "octicon-screen-normal"}} | ||||||
|  | 					{{ctx.Locale.Tr "projects.exit_fullscreen"}} | ||||||
|  | 				</a> | ||||||
| 				<a class="item" href="{{.Link}}/edit?redirect=project"> | 				<a class="item" href="{{.Link}}/edit?redirect=project"> | ||||||
| 					{{svg "octicon-pencil"}} | 					{{svg "octicon-pencil"}} | ||||||
| 					{{ctx.Locale.Tr "repo.issues.label_edit"}} | 					{{ctx.Locale.Tr "repo.issues.label_edit"}} | ||||||
| @@ -56,13 +64,12 @@ | |||||||
| 		{{end}} | 		{{end}} | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
| 	<div class="content">{{$.Project.RenderedContent}}</div> | 	<div class="ui container project-description"> | ||||||
|  | 		{{$.Project.RenderedContent}} | ||||||
|  | 		<div class="divider"></div> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
| 	<div class="divider"></div> | 	<div id="project-board" class="board {{if $canWriteProject}}sortable{{end}}" data-project-borad-writable="{{$canWriteProject}}" {{if $canWriteProject}}data-url="{{$.Link}}/move"{{end}}> | ||||||
| </div> |  | ||||||
|  |  | ||||||
| <div id="project-board" data-project-borad-writable="{{$canWriteProject}}"> |  | ||||||
| 	<div class="board {{if $canWriteProject}}sortable{{end}}" {{if $canWriteProject}}data-url="{{$.Link}}/move"{{end}}> |  | ||||||
| 		{{range .Columns}} | 		{{range .Columns}} | ||||||
| 			<div class="project-column" {{if .Color}}style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> | 			<div class="project-column" {{if .Color}}style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> | ||||||
| 				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}"> | 				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}"> | ||||||
|   | |||||||
| @@ -8,9 +8,7 @@ | |||||||
| 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> | 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="ui container fluid padded"> | 	{{template "projects/view" .}} | ||||||
| 		{{template "projects/view" .}} |  | ||||||
| 	</div> |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||||
|   | |||||||
| @@ -1,31 +1,34 @@ | |||||||
| .board { | #project-board { | ||||||
|   display: flex; |   display: flex; | ||||||
|  |   align-items: stretch; | ||||||
|   flex-direction: row; |   flex-direction: row; | ||||||
|   flex-wrap: nowrap; |   flex-wrap: nowrap; | ||||||
|   overflow-x: auto; |   overflow: auto; | ||||||
|   overflow-y: clip; |  | ||||||
|   align-items: stretch; |  | ||||||
|   margin: 0 0.5em; |   margin: 0 0.5em; | ||||||
|  |   max-height: calc(100vh - 120px); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .project-header { | ||||||
|  |   padding: 0.5em 0; | ||||||
|  |   overflow-x: auto; /* in fullscreen mode, the position is fixed, so we can't use "flex wrap" which would change the height */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .project-header h2 { | ||||||
|  |   white-space: nowrap; | ||||||
|  |   margin: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| .project-column { | .project-column { | ||||||
|   background-color: var(--color-project-column-bg) !important; |  | ||||||
|   border: 1px solid var(--color-secondary) !important; |  | ||||||
|   border-radius: var(--border-radius); |  | ||||||
|   margin: 0 0.5rem !important; |  | ||||||
|   padding: 0.5rem !important; |  | ||||||
|   width: 320px; |  | ||||||
|   height: initial; |  | ||||||
|   min-height: max(calc(100vh - 400px), 300px); |  | ||||||
|   flex: 0 0 auto; |   flex: 0 0 auto; | ||||||
|   overflow: visible; |  | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   cursor: default; |   background-color: var(--color-project-column-bg); | ||||||
| } |   border: 1px solid var(--color-secondary); | ||||||
|  |   border-radius: var(--border-radius); | ||||||
| .project-column .issue-card { |   margin: 0 0.5rem; | ||||||
|   color: var(--color-text); |   padding: 0.5rem; | ||||||
|  |   width: 320px; | ||||||
|  |   overflow: visible; | ||||||
| } | } | ||||||
|  |  | ||||||
| .project-column-header { | .project-column-header { | ||||||
| @@ -39,16 +42,15 @@ | |||||||
|   color: inherit; |   color: inherit; | ||||||
| } | } | ||||||
|  |  | ||||||
| .project-column > .cards { | .project-column > .ui.cards { | ||||||
|   flex: 1; |   flex: 1; | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-content: baseline; |   flex-wrap: nowrap; | ||||||
|   margin: 0 !important; |  | ||||||
|   padding: 0 !important; |  | ||||||
|   flex-wrap: nowrap !important; |  | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   overflow-x: clip; |   overflow: clip auto; | ||||||
|   gap: .25rem; |   gap: .25rem; | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| .project-column > .divider { | .project-column > .divider { | ||||||
| @@ -98,3 +100,29 @@ | |||||||
| .card-ghost * { | .card-ghost * { | ||||||
|   opacity: 0; |   opacity: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .fullscreen.projects-view .project-header { | ||||||
|  |   position: fixed; | ||||||
|  |   z-index: 1000; | ||||||
|  |   top: 0; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  |   padding: 0.5em; | ||||||
|  |   width: 100%; | ||||||
|  |   max-width: 100%; | ||||||
|  |   background-color: var(--color-body); | ||||||
|  |   border-bottom: 1px solid var(--color-secondary); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Hide project-description in full-screen due to its variable height. No need to show it for better space use. */ | ||||||
|  | .fullscreen.projects-view .project-description { | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .fullscreen.projects-view #project-board { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 60px; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  |   max-height: calc(100vh - 70px); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| .ui.menu { | .ui.menu { | ||||||
|   display: flex; |   display: flex; | ||||||
|  |   flex-shrink: 0; | ||||||
|   margin: 1rem 0; |   margin: 1rem 0; | ||||||
|   font-family: var(--fonts-regular); |   font-family: var(--fonts-regular); | ||||||
|   font-weight: var(--font-weight-normal); |   font-weight: var(--font-weight-normal); | ||||||
| @@ -643,6 +644,7 @@ | |||||||
|   display: inline-flex; |   display: inline-flex; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   vertical-align: middle; |   vertical-align: middle; | ||||||
|  |   flex-shrink: 0; | ||||||
| } | } | ||||||
| .ui.compact.vertical.menu { | .ui.compact.vertical.menu { | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import {formatDatetime} from '../utils/time.ts'; | |||||||
| import {renderAnsi} from '../render/ansi.ts'; | import {renderAnsi} from '../render/ansi.ts'; | ||||||
| import {POST, DELETE} from '../modules/fetch.ts'; | import {POST, DELETE} from '../modules/fetch.ts'; | ||||||
| import type {IntervalId} from '../types.ts'; | import type {IntervalId} from '../types.ts'; | ||||||
|  | import {toggleFullScreen} from '../utils.ts'; | ||||||
|  |  | ||||||
| // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" | // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" | ||||||
| type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; | type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; | ||||||
| @@ -416,21 +417,7 @@ export default defineComponent({ | |||||||
|  |  | ||||||
|     toggleFullScreen() { |     toggleFullScreen() { | ||||||
|       this.isFullScreen = !this.isFullScreen; |       this.isFullScreen = !this.isFullScreen; | ||||||
|       const fullScreenEl = document.querySelector('.action-view-right'); |       toggleFullScreen('.action-view-right', this.isFullScreen, '.action-view-body'); | ||||||
|       const outerEl = document.querySelector('.full.height'); |  | ||||||
|       const actionBodyEl = document.querySelector('.action-view-body'); |  | ||||||
|       const headerEl = document.querySelector('#navbar'); |  | ||||||
|       const contentEl = document.querySelector('.page-content'); |  | ||||||
|       const footerEl = document.querySelector('.page-footer'); |  | ||||||
|       toggleElem(headerEl, !this.isFullScreen); |  | ||||||
|       toggleElem(contentEl, !this.isFullScreen); |  | ||||||
|       toggleElem(footerEl, !this.isFullScreen); |  | ||||||
|       // move .action-view-right to new parent |  | ||||||
|       if (this.isFullScreen) { |  | ||||||
|         outerEl.append(fullScreenEl); |  | ||||||
|       } else { |  | ||||||
|         actionBodyEl.append(fullScreenEl); |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     async hashChangeListener() { |     async hashChangeListener() { | ||||||
|       const selectedLogStep = window.location.hash; |       const selectedLogStep = window.location.hash; | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ import {contrastColor} from '../utils/color.ts'; | |||||||
| import {createSortable} from '../modules/sortable.ts'; | import {createSortable} from '../modules/sortable.ts'; | ||||||
| import {POST, request} from '../modules/fetch.ts'; | import {POST, request} from '../modules/fetch.ts'; | ||||||
| import {fomanticQuery} from '../modules/fomantic/base.ts'; | import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||||
| import {queryElemChildren, queryElems} from '../utils/dom.ts'; | import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts'; | ||||||
| import type {SortableEvent} from 'sortablejs'; | import type {SortableEvent} from 'sortablejs'; | ||||||
|  | import {toggleFullScreen} from '../utils.ts'; | ||||||
|  |  | ||||||
| function updateIssueCount(card: HTMLElement): void { | function updateIssueCount(card: HTMLElement): void { | ||||||
|   const parent = card.parentElement; |   const parent = card.parentElement; | ||||||
| @@ -34,8 +35,8 @@ async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<voi | |||||||
| } | } | ||||||
|  |  | ||||||
| async function initRepoProjectSortable(): Promise<void> { | async function initRepoProjectSortable(): Promise<void> { | ||||||
|   // the HTML layout is: #project-board > .board > .project-column .cards > .issue-card |   // the HTML layout is: #project-board.board > .project-column .cards > .issue-card | ||||||
|   const mainBoard = document.querySelector('#project-board > .board.sortable'); |   const mainBoard = document.querySelector('#project-board'); | ||||||
|   let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column'); |   let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column'); | ||||||
|   createSortable(mainBoard, { |   createSortable(mainBoard, { | ||||||
|     group: 'project-column', |     group: 'project-column', | ||||||
| @@ -139,7 +140,24 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function initRepoProjectToggleFullScreen(): void { | ||||||
|  |   const enterFullscreenBtn = document.querySelector('.screen-full'); | ||||||
|  |   const exitFullscreenBtn = document.querySelector('.screen-normal'); | ||||||
|  |   if (!enterFullscreenBtn || !exitFullscreenBtn) return; | ||||||
|  |  | ||||||
|  |   const toggleFullscreenState = (isFullScreen: boolean) => { | ||||||
|  |     toggleFullScreen('.projects-view', isFullScreen); | ||||||
|  |     toggleElem(enterFullscreenBtn, !isFullScreen); | ||||||
|  |     toggleElem(exitFullscreenBtn, isFullScreen); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   enterFullscreenBtn.addEventListener('click', () => toggleFullscreenState(true)); | ||||||
|  |   exitFullscreenBtn.addEventListener('click', () => toggleFullscreenState(false)); | ||||||
|  | } | ||||||
|  |  | ||||||
| export function initRepoProject(): void { | export function initRepoProject(): void { | ||||||
|  |   initRepoProjectToggleFullScreen(); | ||||||
|  |  | ||||||
|   const writableProjectBoard = document.querySelector('#project-board[data-project-borad-writable="true"]'); |   const writableProjectBoard = document.querySelector('#project-board[data-project-borad-writable="true"]'); | ||||||
|   if (!writableProjectBoard) return; |   if (!writableProjectBoard) return; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import {decode, encode} from 'uint8-to-base64'; | import {decode, encode} from 'uint8-to-base64'; | ||||||
| import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts'; | import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts'; | ||||||
|  | import {toggleClass, toggleElem} from './utils/dom.ts'; | ||||||
|  |  | ||||||
| // transform /path/to/file.ext to /path/to | // transform /path/to/file.ext to /path/to | ||||||
| export function dirname(path: string): string { | export function dirname(path: string): string { | ||||||
| @@ -179,3 +180,24 @@ export function isImageFile({name, type}: {name?: string, type?: string}): boole | |||||||
| export function isVideoFile({name, type}: {name?: string, type?: string}): boolean { | export function isVideoFile({name, type}: {name?: string, type?: string}): boolean { | ||||||
|   return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); |   return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function toggleFullScreen(fullscreenElementsSelector: string, isFullScreen: boolean, sourceParentSelector?: string): void { | ||||||
|  |   // hide other elements | ||||||
|  |   const headerEl = document.querySelector('#navbar'); | ||||||
|  |   const contentEl = document.querySelector('.page-content'); | ||||||
|  |   const footerEl = document.querySelector('.page-footer'); | ||||||
|  |   toggleElem(headerEl, !isFullScreen); | ||||||
|  |   toggleElem(contentEl, !isFullScreen); | ||||||
|  |   toggleElem(footerEl, !isFullScreen); | ||||||
|  |  | ||||||
|  |   const sourceParentEl = sourceParentSelector ? document.querySelector(sourceParentSelector) : contentEl; | ||||||
|  |  | ||||||
|  |   const fullScreenEl = document.querySelector(fullscreenElementsSelector); | ||||||
|  |   const outerEl = document.querySelector('.full.height'); | ||||||
|  |   toggleClass(fullscreenElementsSelector, 'fullscreen', isFullScreen); | ||||||
|  |   if (isFullScreen) { | ||||||
|  |     outerEl.append(fullScreenEl); | ||||||
|  |   } else { | ||||||
|  |     sourceParentEl.append(fullScreenEl); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user