diff --git a/src/app/app.tsx b/src/app/app.tsx index 3acaa712..cdea6288 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -30,6 +30,7 @@ type OV = mobx.IObservableValue; @mobxReact.observer class App extends React.Component<{}, {}> { dcWait: OV = mobx.observable.box(false, { name: "dcWait" }); + mainContentRef: React.RefObject = React.createRef(); constructor(props: any) { super(props); @@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> { let hasClientStop = GlobalModel.getHasClientStop(); let dcWait = this.dcWait.get(); let platform = GlobalModel.getPlatform(); + let clientData = GlobalModel.clientData.get(); + + // Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around. + // Doing it here fixes the flicker cos app is not rendered until clientData is populated. + if (clientData == null) { + return null; + } if (disconnected || hasClientStop) { if (!dcWait) { @@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> { } return (
-
- +
+
@@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> { } return (
-
- +
+ diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 299fbd51..9f4b2143 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import cn from "classnames"; import { If } from "tsx-control-statements/components"; -import { RemoteType, StatusIndicatorLevel } from "../../types/types"; +import { RemoteType } from "../../types/types"; import ReactDOM from "react-dom"; -import { GlobalModel } from "../../model/model"; +import { GlobalModel, GlobalCommandRunner } from "../../model/model"; import * as appconst from "../appconst"; import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil"; +import { MagicLayout } from "../magiclayout"; import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg"; import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg"; @@ -1265,6 +1266,169 @@ In order to use Wave's advanced features like unified history and persistent ses }); } +interface ResizableSidebarProps { + parentRef: React.RefObject; + position: "left" | "right"; + enableSnap?: boolean; + className?: string; + children?: (toggleCollapsed: () => void) => React.ReactNode; + toggleCollapse?: () => void; +} + +@mobxReact.observer +class ResizableSidebar extends React.Component { + resizeStartWidth: number = 0; + startX: number = 0; + prevDelta: number = 0; + prevDragDirection: string = null; + disposeReaction: any; + + @boundMethod + startResizing(event: React.MouseEvent) { + event.preventDefault(); + + let { parentRef, position } = this.props; + let parentRect = parentRef.current?.getBoundingClientRect(); + + if (!parentRect) return; + + if (position === "right") { + this.startX = parentRect.right - event.clientX; + } else { + this.startX = event.clientX - parentRect.left; + } + + this.resizeStartWidth = GlobalModel.mainSidebarModel.getWidth(); + document.addEventListener("mousemove", this.onMouseMove); + document.addEventListener("mouseup", this.stopResizing); + + document.body.style.cursor = "col-resize"; + mobx.action(() => { + GlobalModel.mainSidebarModel.isDragging.set(true); + })(); + } + + @boundMethod + onMouseMove(event: MouseEvent) { + event.preventDefault(); + + let { parentRef, enableSnap, position } = this.props; + let parentRect = parentRef.current?.getBoundingClientRect(); + let mainSidebarModel = GlobalModel.mainSidebarModel; + + if (!mainSidebarModel.isDragging.get() || !parentRect) return; + + let delta, newWidth; + + if (position === "right") { + delta = parentRect.right - event.clientX - this.startX; + } else { + delta = event.clientX - parentRect.left - this.startX; + } + + newWidth = this.resizeStartWidth + delta; + + if (enableSnap) { + let minWidth = MagicLayout.MainSidebarMinWidth; + let snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold; + let dragResistance = MagicLayout.MainSidebarDragResistance; + let dragDirection; + + if (delta - this.prevDelta > 0) { + dragDirection = "+"; + } else if (delta - this.prevDelta == 0) { + if (this.prevDragDirection == "+") { + dragDirection = "+"; + } else { + dragDirection = "-"; + } + } else { + dragDirection = "-"; + } + + this.prevDelta = delta; + this.prevDragDirection = dragDirection; + + if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") { + newWidth = snapPoint; + mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false); + } else if (newWidth + dragResistance < snapPoint && dragDirection == "-") { + newWidth = minWidth; + mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true); + } else if (newWidth > snapPoint) { + mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false); + } + } else { + if (newWidth <= MagicLayout.MainSidebarMinWidth) { + mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true); + } else { + mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false); + } + } + } + + @boundMethod + stopResizing() { + let mainSidebarModel = GlobalModel.mainSidebarModel; + + GlobalCommandRunner.clientSetSidebar( + mainSidebarModel.tempWidth.get(), + mainSidebarModel.tempCollapsed.get() + ).finally(() => { + mobx.action(() => { + mainSidebarModel.isDragging.set(false); + })(); + }); + + document.removeEventListener("mousemove", this.onMouseMove); + document.removeEventListener("mouseup", this.stopResizing); + document.body.style.cursor = ""; + } + + @boundMethod + toggleCollapsed() { + let mainSidebarModel = GlobalModel.mainSidebarModel; + + let tempCollapsed = mainSidebarModel.getCollapsed(); + let width = MagicLayout.MainSidebarDefaultWidth; + let newWidth; + if (tempCollapsed) { + newWidth = width; + } else { + newWidth = MagicLayout.MainSidebarMinWidth; + } + + mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, !tempCollapsed); + GlobalCommandRunner.clientSetSidebar(newWidth, !tempCollapsed); + } + + render() { + let { className, children } = this.props; + let mainSidebarModel = GlobalModel.mainSidebarModel; + let width = mainSidebarModel.getWidth(); + let isCollapsed = mainSidebarModel.getCollapsed(); + + return ( +
+
{children(this.toggleCollapsed)}
+
+
+ ); + } +} + export { CmdStrCode, Toggle, @@ -1286,5 +1450,6 @@ export { LinkButton, Status, Modal, + ResizableSidebar, ShowWaveShellInstallPrompt, }; diff --git a/src/app/magiclayout.ts b/src/app/magiclayout.ts index a96cdec6..4b2bb70f 100644 --- a/src/app/magiclayout.ts +++ b/src/app/magiclayout.ts @@ -27,6 +27,12 @@ let MagicLayout = { ScreenSidebarWidthPadding: 5, ScreenSidebarMinWidth: 200, ScreenSidebarHeaderHeight: 28, + + MainSidebarMinWidth: 75, + MainSidebarMaxWidth: 300, + MainSidebarSnapThreshold: 90, + MainSidebarDragResistance: 50, + MainSidebarDefaultWidth: 240, }; let m = MagicLayout; diff --git a/src/app/sidebar/sidebar.less b/src/app/sidebar/sidebar.less index 3946ab94..f91bc188 100644 --- a/src/app/sidebar/sidebar.less +++ b/src/app/sidebar/sidebar.less @@ -3,14 +3,13 @@ .main-sidebar { padding: 0; - min-width: 20rem; - max-width: 20rem; display: flex; flex-direction: column; position: relative; font-size: 12.5px; line-height: 20px; backdrop-filter: blur(4px); + z-index: 20; .title-bar-drag { -webkit-app-region: drag; @@ -24,7 +23,6 @@ &.collapsed { width: 6em; min-width: 6em; - .arrow-container, .collapse-button { transform: rotate(180deg); @@ -34,7 +32,7 @@ margin-top: 26px; .top, - .workspaces-item, + .workspaces, .middle, .bottom, .separator { @@ -50,7 +48,7 @@ justify-content: center; align-items: center; - .logo-container img { + .logo-container { width: 45px; } @@ -86,10 +84,14 @@ display: flex; flex-direction: row; + .logo-container { + flex-shrink: 0; + width: 100px; + } + .spacer { flex-grow: 1; } - img { width: 100px; } @@ -150,7 +152,6 @@ margin-left: 6px; border-radius: 4px; opacity: 1; - transition: opacity 0.1s ease-in-out, visibility 0.1s step-end; width: inherit; max-width: inherit; min-width: inherit; @@ -177,6 +178,7 @@ float: right; margin-right: 6px; letter-spacing: 6px; + margin-left: auto; } &:hover { :not(.disabled) .hotkey { diff --git a/src/app/sidebar/sidebar.tsx b/src/app/sidebar/sidebar.tsx index 5a47e2b5..7f2cf5d6 100644 --- a/src/app/sidebar/sidebar.tsx +++ b/src/app/sidebar/sidebar.tsx @@ -7,7 +7,7 @@ import * as mobx from "mobx"; import { boundMethod } from "autobind-decorator"; import cn from "classnames"; import dayjs from "dayjs"; -import type { RemoteType } from "../../types/types"; +import type { ClientDataType, RemoteType } from "../../types/types"; import { If } from "tsx-control-statements/components"; import { compareLoose } from "semver"; @@ -19,6 +19,7 @@ import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model"; import { isBlank, openLink } from "../../util/util"; +import { ResizableSidebar } from "../common/common"; import * as constants from "../appconst"; import "./sidebar.less"; @@ -26,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common dayjs.extend(localizedFormat); -type OV = mobx.IObservableValue; - class SideBarItem extends React.Component<{ frontIcon: React.ReactNode; contents: React.ReactNode | string; @@ -59,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> { } } -@mobxReact.observer -class MainSideBar extends React.Component<{}, {}> { - collapsed: mobx.IObservableValue = mobx.observable.box(false); +interface MainSideBarProps { + parentRef: React.RefObject; + clientData: ClientDataType; +} - @boundMethod - toggleCollapsed() { - mobx.action(() => { - this.collapsed.set(!this.collapsed.get()); - })(); - } +@mobxReact.observer +class MainSideBar extends React.Component { + sidebarRef = React.createRef(); handleSessionClick(sessionId: string) { GlobalCommandRunner.switchSession(sessionId); @@ -208,106 +205,116 @@ class MainSideBar extends React.Component<{}, {}> { } render() { - const isCollapsed = this.collapsed.get(); - const clientData = GlobalModel.clientData.get(); + let clientData = this.props.clientData; let needsUpdate = false; if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) { needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0; } + let mainSidebar = GlobalModel.mainSidebarModel; + let isCollapsed = mainSidebar.getCollapsed(); return ( -
-
-
-
- -
- + + {(toggleCollapse) => ( + +
+
+
+ +
+ +
+
+ +
+ +
+
+
+ +
+
-
- -
- +
+
+ } + contents="History" + endIcons={[]} + onClick={this.handleHistoryClick} + /> + {/* } contents="Favorites" endIcon={⌘B} onClick={this.handleBookmarksClick}/> */} + } + contents="Connections" + onClick={this.handleConnectionsClick} + />
-
-
- -
- -
-
-
- } - contents="History" - endIcons={[]} - onClick={this.handleHistoryClick} - /> - {/* } contents="Favorites" endIcon={⌘B} onClick={this.handleBookmarksClick}/> */} - } - contents="Connections" - onClick={this.handleConnectionsClick} - /> -
-
- } - contents="Workspaces" - endIcons={[ - - - , - ]} - /> -
{this.getSessions()}
-
- +
} - contents="Update Available" - onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")} + key="workspaces" + className="workspaces" + frontIcon={} + contents="Workspaces" + endIcons={[ + + + , + ]} /> - - - } - contents="Apps" - onClick={this.handlePluginsClick} - endIcons={[]} - /> - - } - contents="Settings" - onClick={this.handleSettingsClick} - /> - } - contents="Documentation" - onClick={() => openLink("https://docs.waveterm.dev")} - /> - } - contents="Discord" - onClick={() => openLink("https://discord.gg/XfvZ334gwU")} - /> -
-
-
+
{this.getSessions()}
+
+ + } + contents="Update Available" + onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")} + /> + + + } + contents="Apps" + onClick={this.handlePluginsClick} + endIcons={[]} + /> + + } + contents="Settings" + onClick={this.handleSettingsClick} + /> + } + contents="Documentation" + onClick={() => openLink("https://docs.waveterm.dev")} + /> + } + contents="Discord" + onClick={() => openLink("https://discord.gg/XfvZ334gwU")} + /> +
+
+ + )} + ); } } diff --git a/src/app/workspace/workspace.less b/src/app/workspace/workspace.less index 9365560a..af57ba15 100644 --- a/src/app/workspace/workspace.less +++ b/src/app/workspace/workspace.less @@ -8,12 +8,12 @@ &.is-hidden { display: none; } - max-width: calc(100% - 20.5em); background: @background-session; border: 1px solid @base-border; border-radius: 8px; - transition: width 0.2s ease; + // transition: width 0.2s ease; margin-bottom: 0.5em; + margin-right: 0.5em; .center-message { display: flex; @@ -24,6 +24,3 @@ color: @text-secondary; } } -.collapsed + .session-view { - max-width: calc(100% - 6.7em); -} diff --git a/src/app/workspace/workspaceview.tsx b/src/app/workspace/workspaceview.tsx index ca494e7c..967104f1 100644 --- a/src/app/workspace/workspaceview.tsx +++ b/src/app/workspace/workspaceview.tsx @@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> { if (session == null) { return (
-
(no active workspace)
+
+
(no active workspace)
+
); } let activeScreen = session.getActiveScreen(); let cmdInputHeight = model.inputModel.cmdInputHeight.get(); if (cmdInputHeight == 0) { - cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools) + cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools) } - cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px + cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px let isHidden = GlobalModel.activeMainView.get() != "session"; + let mainSidebarModel = GlobalModel.mainSidebarModel; + + // Has to calc manually because when tabs overflow, the width of the session view is increased for some reason causing inconsistent width. + // 6px is the right margin of session view. + let width = window.innerWidth - 6 - mainSidebarModel.getWidth(); return ( -
+
diff --git a/src/model/model.ts b/src/model/model.ts index 38ab7121..384d0329 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js"; import { v4 as uuidv4 } from "uuid"; import { boundMethod } from "autobind-decorator"; import { debounce } from "throttle-debounce"; +import * as mobxReact from "mobx-react"; import { handleJsonFetchResponse, base64ToString, @@ -2617,6 +2618,74 @@ class ClientSettingsViewModel { })(); } } +class MainSidebarModel { + tempWidth: OV = mobx.observable.box(null, { + name: "MainSidebarModel-tempWidth", + }); + tempCollapsed: OV = mobx.observable.box(null, { + name: "MainSidebarModel-tempCollapsed", + }); + isDragging: OV = mobx.observable.box(false, { + name: "MainSidebarModel-isDragging", + }); + + setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void { + let width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth)); + + mobx.action(() => { + this.tempWidth.set(width); + this.tempCollapsed.set(newCollapsed); + })(); + } + + getWidth(): number { + let clientData = GlobalModel.clientData.get(); + let width = clientData.clientopts.mainsidebar.width; + if (this.isDragging.get()) { + if (this.tempWidth.get() == null && width == null) { + return MagicLayout.MainSidebarDefaultWidth; + } + if (this.tempWidth.get() == null) { + return width; + } + return this.tempWidth.get(); + } + // Set by CLI and collapsed + if (this.getCollapsed() && width != MagicLayout.MainSidebarMinWidth) { + this.setTempWidthAndTempCollapsed(MagicLayout.MainSidebarMinWidth, true); + return MagicLayout.MainSidebarMinWidth; + } + // Set by CLI and not collapsed + if (!this.getCollapsed()) { + if (width <= MagicLayout.MainSidebarMinWidth) { + width = MagicLayout.MainSidebarDefaultWidth; + } + let snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold; + if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) { + width = MagicLayout.MainSidebarDefaultWidth; + } + this.setTempWidthAndTempCollapsed(width, false); + return width; + } + this.setTempWidthAndTempCollapsed(width, this.getCollapsed()); + return width; + } + + getCollapsed(): boolean { + let clientData = GlobalModel.clientData.get(); + let collapsed = clientData.clientopts.mainsidebar.collapsed; + if (this.isDragging.get()) { + if (this.tempCollapsed.get() == null && collapsed == null) { + return false; + } + if (this.tempCollapsed.get() == null) { + return collapsed; + } + return this.tempCollapsed.get(); + } + return collapsed; + } +} class BookmarksModel { bookmarks: OArr = mobx.observable.array([], { @@ -3387,6 +3456,7 @@ class Model { connectionViewModel: ConnectionsViewModel; clientSettingsViewModel: ClientSettingsViewModel; modalsModel: ModalsModel; + mainSidebarModel: MainSidebarModel; clientData: OV = mobx.observable.box(null, { name: "clientData", }); @@ -3413,6 +3483,7 @@ class Model { this.remotesModalModel = new RemotesModalModel(); this.remotesModel = new RemotesModel(); this.modalsModel = new ModalsModel(); + this.mainSidebarModel = new MainSidebarModel(); let isWaveSrvRunning = getApi().getWaveSrvStatus(); this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { name: "model-wavesrv-running", @@ -4980,6 +5051,11 @@ class CommandRunner { return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false); } + clientSetSidebar(width: number, collapsed: boolean): Promise { + let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" }; + return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false); + } + editBookmark(bookmarkId: string, desc: string, cmdstr: string) { let kwargs = { nohist: "1", diff --git a/src/types/types.ts b/src/types/types.ts index 53a18fbe..e1bd7276 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -524,6 +524,10 @@ type ClientOptsType = { noreleasecheck: boolean; acceptedtos: number; confirmflags: ConfirmFlagsType; + mainsidebar: { + collapsed: boolean; + width: number; + }; }; type ReleaseInfoType = { diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 6a13924c..6f681177 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -92,6 +92,7 @@ var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass" var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} var ConfirmFlags = []string{"hideshellprompt"} +var SidebarNames = []string{"main"} var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} @@ -218,6 +219,7 @@ func init() { registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand) registerCmdFn("client:accepttos", ClientAcceptTosCommand) registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand) + registerCmdFn("client:setsidebar", ClientSetSidebarCommand) registerCmdFn("sidebar:open", SidebarOpenCommand) registerCmdFn("sidebar:close", SidebarCloseCommand) @@ -4463,6 +4465,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT return update, nil } +func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v", err) + } + + // Handle collapsed + collapsed, ok := pk.Kwargs["collapsed"] + if !ok { + return nil, fmt.Errorf("collapsed key not provided") + } + collapsedValue := resolveBool(collapsed, false) + + // Handle width + var width int + if w, exists := pk.Kwargs["width"]; exists { + width, err = resolveNonNegInt(w, 0) + if err != nil { + return nil, fmt.Errorf("error resolving width: %v", err) + } + } else if clientData.ClientOpts.MainSidebar != nil { + width = clientData.ClientOpts.MainSidebar.Width + } + + // Initialize SidebarCollapsed if it's nil + if clientData.ClientOpts.MainSidebar == nil { + clientData.ClientOpts.MainSidebar = new(sstore.SidebarValueType) + } + + // Set the sidebar values + var sv sstore.SidebarValueType + sv.Collapsed = collapsedValue + if width != 0 { + sv.Width = width + } + clientData.ClientOpts.MainSidebar = &sv + + // Update client data + err = sstore.SetClientOpts(ctx, clientData.ClientOpts) + if err != nil { + return nil, fmt.Errorf("error updating client data: %v", err) + } + + // Retrieve updated client data + clientData, err = sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve updated client data: %v", err) + } + + update := &sstore.ModelUpdate{ + ClientData: clientData, + } + + return update, nil +} + func validateOpenAIAPIToken(key string) error { if len(key) > MaxOpenAIAPITokenLen { return fmt.Errorf("invalid openai token, too long") diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index 863e97f5..9cd62e50 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -275,11 +275,17 @@ func (tdata *TelemetryData) Scan(val interface{}) error { return quickScanJson(tdata, val) } +type SidebarValueType struct { + Collapsed bool `json:"collapsed"` + Width int `json:"width"` +} + type ClientOptsType struct { - NoTelemetry bool `json:"notelemetry,omitempty"` - NoReleaseCheck bool `json:"noreleasecheck,omitempty"` - AcceptedTos int64 `json:"acceptedtos,omitempty"` - ConfirmFlags map[string]bool `json:"confirmflags,omitempty"` + NoTelemetry bool `json:"notelemetry,omitempty"` + NoReleaseCheck bool `json:"noreleasecheck,omitempty"` + AcceptedTos int64 `json:"acceptedtos,omitempty"` + ConfirmFlags map[string]bool `json:"confirmflags,omitempty"` + MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"` } type FeOptsType struct {