M11: starter templates: Templates tab, first-run templates and Getting started card on the home screen
This commit is contained in:
parent
c2a67f81e5
commit
573dd521bb
|
|
@ -150,7 +150,10 @@ fn main() -> eframe::Result {
|
|||
// default, never a build from source unless asked).
|
||||
session.live.links = figcraft_collab_net::links::system();
|
||||
session.invite_links_startup();
|
||||
if session.docs.is_empty() {
|
||||
// A first run (no settings kept yet, nothing to open) starts on the home screen, with its
|
||||
// tips and templates; later runs start in a new file.
|
||||
let first_run = svc.prefs.is_none() && args.join.is_none();
|
||||
if session.docs.is_empty() && !first_run {
|
||||
let _ = session.execute("file.new", &json!({}));
|
||||
}
|
||||
let mut fig = FigApp::new(session, Box::new(svc));
|
||||
|
|
|
|||
|
|
@ -257,7 +257,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "Kommentar"
|
||||
"Comments" = "Kommentare"
|
||||
"Commits" = "Commits"
|
||||
"Community" = "Community"
|
||||
"Company" = "Unternehmen"
|
||||
"Compare changes" = "Änderungen vergleichen"
|
||||
"Completed" = "Abgeschlossen"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ name = "English"
|
|||
"1…9, 0" = "1…9, 0"
|
||||
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}"
|
||||
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "A collection of this file with these modes and variables: override their values per mode and add variables of its own"
|
||||
"A responsive product page: hero, features, figures, a quote and a footer" = "A responsive product page: hero, features, figures, a quote and a footer"
|
||||
"A spring plays until it settles: its mass, stiffness and damping set how long" = "A spring plays until it settles: its mass, stiffness and damping set how long"
|
||||
"A title, an agenda, a big figure, a chart and a closing slide" = "A title, an agenda, a big figure, a chart and a closing slide"
|
||||
"About" = "About"
|
||||
"About FigCraft" = "About FigCraft"
|
||||
"About this server" = "About this server"
|
||||
|
|
@ -88,6 +90,7 @@ name = "English"
|
|||
"All variables" = "All variables"
|
||||
"Alpha" = "Alpha"
|
||||
"Alt+drag" = "Alt+drag"
|
||||
"An analytics screen on a grid: figures, charts drawn from vectors and a table" = "An analytics screen on a grid: figures, charts drawn from vectors and a table"
|
||||
"An invite link that opens FigCraft and joins" = "An invite link that opens FigCraft and joins"
|
||||
"Anchor" = "Anchor"
|
||||
"Android" = "Android"
|
||||
|
|
@ -106,6 +109,7 @@ name = "English"
|
|||
"Apply variable" = "Apply variable"
|
||||
"Apply variable to text" = "Apply variable to text"
|
||||
"Apply variable to visibility" = "Apply variable to visibility"
|
||||
"Apps" = "Apps"
|
||||
"Arc" = "Arc"
|
||||
"Arc ratio" = "Arc ratio"
|
||||
"Arc start" = "Arc start"
|
||||
|
|
@ -189,6 +193,7 @@ name = "English"
|
|||
"Clear from this list" = "Clear from this list"
|
||||
"Code or invite link" = "Code or invite link"
|
||||
"Color profile: {name}" = "Color profile: {name}"
|
||||
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Colour and text styles, Light and Dark variables, buttons, inputs and cards"
|
||||
"CSS custom properties…" = "CSS custom properties…"
|
||||
"Can edit" = "Can edit"
|
||||
"Can't read this library: {e}" = "Can't read this library: {e}"
|
||||
|
|
@ -256,7 +261,6 @@ name = "English"
|
|||
"Comment" = "Comment"
|
||||
"Comments" = "Comments"
|
||||
"Commits" = "Commits"
|
||||
"Community" = "Community"
|
||||
"Company" = "Company"
|
||||
"Compare changes" = "Compare changes"
|
||||
"Completed" = "Completed"
|
||||
|
|
@ -327,6 +331,7 @@ name = "English"
|
|||
"Dark" = "Dark"
|
||||
"Darken" = "Darken"
|
||||
"Dash" = "Dash"
|
||||
"Dashboard" = "Dashboard"
|
||||
"Decoration style" = "Decoration style"
|
||||
"Decoration" = "Decoration"
|
||||
"Deep select" = "Deep select"
|
||||
|
|
@ -353,6 +358,7 @@ name = "English"
|
|||
"Description" = "Description"
|
||||
"Description (optional)" = "Description (optional)"
|
||||
"Design" = "Design"
|
||||
"Design system starter" = "Design system starter"
|
||||
"Desktop" = "Desktop"
|
||||
"Destination" = "Destination"
|
||||
"Detach alias" = "Detach alias"
|
||||
|
|
@ -388,6 +394,7 @@ name = "English"
|
|||
"Drafts" = "Drafts"
|
||||
"Drag the stroke to add a width point" = "Drag the stroke to add a width point"
|
||||
"Draw" = "Draw"
|
||||
"Draw a frame" = "Draw a frame"
|
||||
"Drawing tools" = "Drawing tools"
|
||||
"Drop shadow" = "Drop shadow"
|
||||
"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community."
|
||||
|
|
@ -436,6 +443,7 @@ name = "English"
|
|||
"Expand" = "Expand"
|
||||
"Export" = "Export"
|
||||
"Export all" = "Export all"
|
||||
"Export frames and layers as PNG, JPG, SVG or PDF" = "Export frames and layers as PNG, JPG, SVG or PDF"
|
||||
"Export page as PNG" = "Export page as PNG"
|
||||
"Export settings" = "Export settings"
|
||||
"Export variables" = "Export variables"
|
||||
|
|
@ -504,6 +512,7 @@ name = "English"
|
|||
"Font size" = "Font size"
|
||||
"Font style" = "Font style"
|
||||
"Forget on this computer" = "Forget on this computer"
|
||||
"Four screens of a trail app with components and a prototype flow" = "Four screens of a trail app with components and a prototype flow"
|
||||
"Fractions" = "Fractions"
|
||||
"Frame" = "Frame"
|
||||
"Frame selection" = "Frame selection"
|
||||
|
|
@ -517,6 +526,7 @@ name = "English"
|
|||
"Gap between columns" = "Gap between columns"
|
||||
"Gap between items" = "Gap between items"
|
||||
"Gap between rows" = "Gap between rows"
|
||||
"Getting started" = "Getting started"
|
||||
"Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)." = "Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)."
|
||||
"Glass" = "Glass"
|
||||
"Go to main component" = "Go to main component"
|
||||
|
|
@ -525,6 +535,7 @@ name = "English"
|
|||
"Gradient" = "Gradient"
|
||||
"Gray" = "Gray"
|
||||
"Green" = "Green"
|
||||
"Greyscale components and two example screens to sketch a flow with" = "Greyscale components and two example screens to sketch a flow with"
|
||||
"Grid" = "Grid"
|
||||
"Grid ({size})" = "Grid ({size})"
|
||||
"Grid styles" = "Grid styles"
|
||||
|
|
@ -546,6 +557,7 @@ name = "English"
|
|||
"Hide contents" = "Hide contents"
|
||||
"Hide from publishing" = "Hide from publishing"
|
||||
"Hide section contents" = "Hide section contents"
|
||||
"Hide these tips (Getting started in the Help menu shows them again)" = "Hide these tips (Getting started in the Help menu shows them again)"
|
||||
"Hide when publishing" = "Hide when publishing"
|
||||
"Highlight layers on hover" = "Highlight layers on hover"
|
||||
"Highlights" = "Highlights"
|
||||
|
|
@ -568,6 +580,8 @@ name = "English"
|
|||
"Hue" = "Hue"
|
||||
"Hug" = "Hug"
|
||||
"Hug contents" = "Hug contents"
|
||||
"Icon components on a 24 px grid with keylines and a size preview" = "Icon components on a 24 px grid with keylines and a size preview"
|
||||
"Icon set starter" = "Icon set starter"
|
||||
"If" = "If"
|
||||
"If {condition}" = "If {condition}"
|
||||
"Ignore auto layout (absolute position)" = "Ignore auto layout (absolute position)"
|
||||
|
|
@ -631,6 +645,8 @@ name = "English"
|
|||
"Key/Gamepad" = "Key/Gamepad"
|
||||
"Key/Gamepad {key}" = "Key/Gamepad {key}"
|
||||
"Keyboard shortcuts" = "Keyboard shortcuts"
|
||||
"Kits" = "Kits"
|
||||
"Landing page" = "Landing page"
|
||||
"Language" = "Language"
|
||||
"Lasso" = "Lasso"
|
||||
"Last" = "Last"
|
||||
|
|
@ -674,6 +690,7 @@ name = "English"
|
|||
"Link" = "Link"
|
||||
"Link copied" = "Link copied"
|
||||
"Link docs, code or tickets for developers." = "Link docs, code or tickets for developers."
|
||||
"Link frames on the Prototype tab, then present the flow" = "Link frames on the Prototype tab, then present the flow"
|
||||
"Link this component to its code component: instances then show your real snippet with their values." = "Link this component to its code component: instances then show your real snippet with their values."
|
||||
"List spacing" = "List spacing"
|
||||
"List style" = "List style"
|
||||
|
|
@ -691,6 +708,7 @@ name = "English"
|
|||
"Main" = "Main"
|
||||
"Main component deleted" = "Main component deleted"
|
||||
"Main menu" = "Main menu"
|
||||
"Make a component" = "Make a component"
|
||||
"Make slot" = "Make slot"
|
||||
"Margin" = "Margin"
|
||||
"Mark all as read" = "Mark all as read"
|
||||
|
|
@ -725,6 +743,7 @@ name = "English"
|
|||
"Mixed" = "Mixed"
|
||||
"Mixed fonts" = "Mixed fonts"
|
||||
"Mixed interactions: click + to replace them" = "Mixed interactions: click + to replace them"
|
||||
"Mobile app" = "Mobile app"
|
||||
"Mode" = "Mode"
|
||||
"Model" = "Model"
|
||||
"Models" = "Models"
|
||||
|
|
@ -796,6 +815,7 @@ name = "English"
|
|||
"No string variables yet. Create them in Local variables." = "No string variables yet. Create them in Local variables."
|
||||
"No styles yet." = "No styles yet."
|
||||
"No styles yet. Click + to create one from the selection." = "No styles yet. Click + to create one from the selection."
|
||||
"No templates match your search" = "No templates match your search"
|
||||
"No text styles yet. Click + to create one from the selected text." = "No text styles yet. Click + to create one from the selected text."
|
||||
"No variables fit here." = "No variables fit here."
|
||||
"No variables yet. Create them in Local variables." = "No variables yet. Create them in Local variables."
|
||||
|
|
@ -830,6 +850,7 @@ name = "English"
|
|||
"On click" = "On click"
|
||||
"On drag" = "On drag"
|
||||
"On this page" = "On this page"
|
||||
"One announcement in five common sizes: square, portrait, story, link and banner" = "One announcement in five common sizes: square, portrait, story, link and banner"
|
||||
"Online" = "Online"
|
||||
"Online files" = "Online files"
|
||||
"Online sharing is off: files are shared on the local network only." = "Online sharing is off: files are shared on the local network only."
|
||||
|
|
@ -902,6 +923,7 @@ name = "English"
|
|||
"Pick color" = "Pick color"
|
||||
"Pick color (I)" = "Pick color (I)"
|
||||
"Pick one font family first" = "Pick one font family first"
|
||||
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Pick the Frame tool, then drag on the canvas or choose a device size"
|
||||
"Pin a property" = "Pin a property"
|
||||
"Pinch" = "Pinch"
|
||||
"Pixel grid" = "Pixel grid"
|
||||
|
|
@ -927,6 +949,7 @@ name = "English"
|
|||
"Present or preview" = "Present or preview"
|
||||
"Present this flow" = "Present this flow"
|
||||
"Presentation" = "Presentation"
|
||||
"Presentation slides" = "Presentation slides"
|
||||
"Presenting: comments" = "Presenting: comments"
|
||||
"Presenting: cycle the size options" = "Presenting: cycle the size options"
|
||||
"Presenting: full screen" = "Presenting: full screen"
|
||||
|
|
@ -1065,6 +1088,9 @@ name = "English"
|
|||
"Rulers" = "Rulers"
|
||||
"Run" = "Run"
|
||||
"Run {label}" = "Run {label}"
|
||||
"Search templates" = "Search templates"
|
||||
"See all templates" = "See all templates"
|
||||
"Select layers and they space themselves as their content changes" = "Select layers and they space themselves as their content changes"
|
||||
"SHA-256: {sha}…" = "SHA-256: {sha}…"
|
||||
"Same as layer" = "Same as layer"
|
||||
"Saturation" = "Saturation"
|
||||
|
|
@ -1124,6 +1150,7 @@ name = "English"
|
|||
"Shape tools" = "Shape tools"
|
||||
"Share" = "Share"
|
||||
"Share this file" = "Share this file"
|
||||
"Share with a code" = "Share with a code"
|
||||
"Share: live session, export" = "Share: live session, export"
|
||||
"Shared" = "Shared"
|
||||
"Shared: copy its code from the file's menu" = "Shared: copy its code from the file's menu"
|
||||
|
|
@ -1133,6 +1160,7 @@ name = "English"
|
|||
"Sharing online · {people}" = "Sharing online · {people}"
|
||||
"Sharing stopped: the file stays on the server, open only to you" = "Sharing stopped: the file stays on the server, open only to you"
|
||||
"Show" = "Show"
|
||||
"Show all templates" = "Show all templates"
|
||||
"Show as grid" = "Show as grid"
|
||||
"Show as list" = "Show as list"
|
||||
"Show autosaves" = "Show autosaves"
|
||||
|
|
@ -1165,7 +1193,9 @@ name = "English"
|
|||
"Small caps" = "Small caps"
|
||||
"Smart animate" = "Smart animate"
|
||||
"Snap to pixel grid" = "Snap to pixel grid"
|
||||
"Social and slides" = "Social and slides"
|
||||
"Social media" = "Social media"
|
||||
"Social post sizes" = "Social post sizes"
|
||||
"Soft light" = "Soft light"
|
||||
"Solid" = "Solid"
|
||||
"Some variants have the same property values: give each one a unique combination" = "Some variants have the same property values: give each one a unique combination"
|
||||
|
|
@ -1190,9 +1220,11 @@ name = "English"
|
|||
"Start" = "Start"
|
||||
"Start ascending from / stop descending at" = "Start ascending from / stop descending at"
|
||||
"Start from a blank canvas" = "Start from a blank canvas"
|
||||
"Start from a template" = "Start from a template"
|
||||
"Start on path" = "Start on path"
|
||||
"Start point" = "Start point"
|
||||
"Start sharing" = "Start sharing"
|
||||
"Start sharing with the Share button, then pass on the code it gives" = "Start sharing with the Share button, then pass on the code it gives"
|
||||
"Starting to share…" = "Starting to share…"
|
||||
"Status" = "Status"
|
||||
"Sticky" = "Sticky"
|
||||
|
|
@ -1241,6 +1273,7 @@ name = "English"
|
|||
"Tell the server's operator what is wrong with it" = "Tell the server's operator what is wrong with it"
|
||||
"Temperature" = "Temperature"
|
||||
"Template" = "Template"
|
||||
"Templates" = "Templates"
|
||||
"Terms" = "Terms"
|
||||
"Text" = "Text"
|
||||
"Text align" = "Text align"
|
||||
|
|
@ -1298,6 +1331,8 @@ name = "English"
|
|||
"Truncate with an ellipsis" = "Truncate with an ellipsis"
|
||||
"Try again" = "Try again"
|
||||
"Try another search or filter." = "Try another search or filter."
|
||||
"Try another word, or look at every kind." = "Try another word, or look at every kind."
|
||||
"Turn a layer into a component, reuse it, and change every copy at once" = "Turn a layer into a component, reuse it, and change every copy at once"
|
||||
"Two digits" = "Two digits"
|
||||
"Type" = "Type"
|
||||
"Type settings" = "Type settings"
|
||||
|
|
@ -1368,6 +1403,7 @@ name = "English"
|
|||
"Watch" = "Watch"
|
||||
"Wavy" = "Wavy"
|
||||
"Web" = "Web"
|
||||
"Websites" = "Websites"
|
||||
"WebSocket URL" = "WebSocket URL"
|
||||
"WebSocket URL copied" = "WebSocket URL copied"
|
||||
"Wedge" = "Wedge"
|
||||
|
|
@ -1385,6 +1421,7 @@ name = "English"
|
|||
"Width" = "Width"
|
||||
"Width point" = "Width point"
|
||||
"Width profile" = "Width profile"
|
||||
"Wireframe kit" = "Wireframe kit"
|
||||
"With the layers' export settings" = "With the layers' export settings"
|
||||
"Wrap" = "Wrap"
|
||||
"Wrap in new section" = "Wrap in new section"
|
||||
|
|
@ -1511,4 +1548,4 @@ name = "English"
|
|||
"⌘ arrows" = "⌘ arrows"
|
||||
"⌘ click" = "⌘ click"
|
||||
"⌘ scroll" = "⌘ scroll"
|
||||
"⌥ drag" = "⌥ drag"
|
||||
"⌥ drag" = "⌥ drag"
|
||||
|
|
@ -17,7 +17,9 @@ status = "draft — needs native review"
|
|||
"1…9, 0" = "1…9, 0"
|
||||
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR, {commits} commits, +{added} / −{deleted} líneas (Δ {delta}), +{bin_added} / −{bin_deleted} recursos binarios, {first} – {last}"
|
||||
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Una colección de este archivo con estos modos y variables: modifica sus valores por modo y añade variables propias"
|
||||
"A responsive product page: hero, features, figures, a quote and a footer" = "Una página de producto adaptable: cabecera, funciones, cifras, una cita y un pie"
|
||||
"A spring plays until it settles: its mass, stiffness and damping set how long" = "Un muelle se reproduce hasta que se asienta: su masa, rigidez y amortiguación fijan cuánto dura"
|
||||
"A title, an agenda, a big figure, a chart and a closing slide" = "Un título, una agenda, una gran cifra, un gráfico y una diapositiva de cierre"
|
||||
"About" = "Acerca de"
|
||||
"About FigCraft" = "Acerca de FigCraft"
|
||||
"About this server" = "Acerca de este servidor"
|
||||
|
|
@ -89,6 +91,7 @@ status = "draft — needs native review"
|
|||
"All variables" = "Todas las variables"
|
||||
"Alpha" = "Alfa"
|
||||
"Alt+drag" = "Alt+arrastrar"
|
||||
"An analytics screen on a grid: figures, charts drawn from vectors and a table" = "Una pantalla de analítica en cuadrícula: cifras, gráficos dibujados con vectores y una tabla"
|
||||
"An invite link that opens FigCraft and joins" = "Un enlace de invitación que abre FigCraft y se une"
|
||||
"Anchor" = "Ancla"
|
||||
"Android" = "Android"
|
||||
|
|
@ -107,6 +110,7 @@ status = "draft — needs native review"
|
|||
"Apply variable" = "Aplicar variable"
|
||||
"Apply variable to text" = "Aplicar variable al texto"
|
||||
"Apply variable to visibility" = "Aplicar variable a la visibilidad"
|
||||
"Apps" = "Aplicaciones"
|
||||
"Arc" = "Arco"
|
||||
"Arc ratio" = "Proporción del arco"
|
||||
"Arc start" = "Inicio del arco"
|
||||
|
|
@ -190,6 +194,7 @@ status = "draft — needs native review"
|
|||
"Clear from this list" = "Borrar de esta lista"
|
||||
"Code or invite link" = "Código o enlace de invitación"
|
||||
"Color profile: {name}" = "Perfil de color: {name}"
|
||||
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Estilos de color y texto, variables Claro y Oscuro, botones, campos y tarjetas"
|
||||
"CSS custom properties…" = "Propiedades personalizadas CSS…"
|
||||
"Can edit" = "Puede editar"
|
||||
"Can't read this library: {e}" = "No se puede leer esta biblioteca: {e}"
|
||||
|
|
@ -257,7 +262,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "Comentario"
|
||||
"Comments" = "Comentarios"
|
||||
"Commits" = "Commits"
|
||||
"Community" = "Comunidad"
|
||||
"Company" = "Empresa"
|
||||
"Compare changes" = "Comparar cambios"
|
||||
"Completed" = "Completado"
|
||||
|
|
@ -328,6 +332,7 @@ status = "draft — needs native review"
|
|||
"Dark" = "Oscuro"
|
||||
"Darken" = "Oscurecer"
|
||||
"Dash" = "Discontinuo"
|
||||
"Dashboard" = "Panel de control"
|
||||
"Decoration style" = "Estilo de decoración"
|
||||
"Decoration" = "Decoración"
|
||||
"Deep select" = "Selección profunda"
|
||||
|
|
@ -354,6 +359,7 @@ status = "draft — needs native review"
|
|||
"Description" = "Descripción"
|
||||
"Description (optional)" = "Descripción (opcional)"
|
||||
"Design" = "Diseño"
|
||||
"Design system starter" = "Base de sistema de diseño"
|
||||
"Desktop" = "Escritorio"
|
||||
"Destination" = "Destino"
|
||||
"Detach alias" = "Separar alias"
|
||||
|
|
@ -389,6 +395,7 @@ status = "draft — needs native review"
|
|||
"Drafts" = "Borradores"
|
||||
"Drag the stroke to add a width point" = "Arrastra el trazo para añadir un punto de ancho"
|
||||
"Draw" = "Dibujar"
|
||||
"Draw a frame" = "Dibuja un marco"
|
||||
"Drawing tools" = "Herramientas de dibujo"
|
||||
"Drop shadow" = "Sombra paralela"
|
||||
"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Con doble licencia MIT OR Apache-2.0. Hecho por el equipo y la comunidad de ArtCraft."
|
||||
|
|
@ -437,6 +444,7 @@ status = "draft — needs native review"
|
|||
"Expand" = "Expandir"
|
||||
"Export" = "Exportar"
|
||||
"Export all" = "Exportar todo"
|
||||
"Export frames and layers as PNG, JPG, SVG or PDF" = "Exporta marcos y capas como PNG, JPG, SVG o PDF"
|
||||
"Export page as PNG" = "Exportar página como PNG"
|
||||
"Export settings" = "Ajustes de exportación"
|
||||
"Export variables" = "Exportar variables"
|
||||
|
|
@ -505,6 +513,7 @@ status = "draft — needs native review"
|
|||
"Font size" = "Tamaño de fuente"
|
||||
"Font style" = "Estilo de fuente"
|
||||
"Forget on this computer" = "Olvidar en este ordenador"
|
||||
"Four screens of a trail app with components and a prototype flow" = "Cuatro pantallas de una app de senderos con componentes y un flujo de prototipo"
|
||||
"Fractions" = "Fracciones"
|
||||
"Frame" = "Marco"
|
||||
"Frame selection" = "Enmarcar selección"
|
||||
|
|
@ -518,6 +527,7 @@ status = "draft — needs native review"
|
|||
"Gap between columns" = "Espacio entre columnas"
|
||||
"Gap between items" = "Espacio entre elementos"
|
||||
"Gap between rows" = "Espacio entre filas"
|
||||
"Getting started" = "Primeros pasos"
|
||||
"Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)." = "Indica la dirección de un servidor abierto al público, o la dirección de un servidor con su clave (`figcraft-server key` la muestra)."
|
||||
"Glass" = "Vidrio"
|
||||
"Go to main component" = "Ir al componente principal"
|
||||
|
|
@ -526,6 +536,7 @@ status = "draft — needs native review"
|
|||
"Gradient" = "Degradado"
|
||||
"Gray" = "Gris"
|
||||
"Green" = "Verde"
|
||||
"Greyscale components and two example screens to sketch a flow with" = "Componentes en escala de grises y dos pantallas de ejemplo para esbozar un flujo"
|
||||
"Grid" = "Cuadrícula"
|
||||
"Grid ({size})" = "Cuadrícula ({size})"
|
||||
"Grid styles" = "Estilos de cuadrícula"
|
||||
|
|
@ -547,6 +558,7 @@ status = "draft — needs native review"
|
|||
"Hide contents" = "Ocultar contenido"
|
||||
"Hide from publishing" = "Ocultar al publicar"
|
||||
"Hide section contents" = "Ocultar contenido de la sección"
|
||||
"Hide these tips (Getting started in the Help menu shows them again)" = "Ocultar estos consejos (Primeros pasos en el menú Ayuda los vuelve a mostrar)"
|
||||
"Hide when publishing" = "Ocultar al publicar"
|
||||
"Highlight layers on hover" = "Resaltar capas al pasar el cursor"
|
||||
"Highlights" = "Luces"
|
||||
|
|
@ -569,6 +581,8 @@ status = "draft — needs native review"
|
|||
"Hue" = "Tono"
|
||||
"Hug" = "Ajustar"
|
||||
"Hug contents" = "Ajustar al contenido"
|
||||
"Icon components on a 24 px grid with keylines and a size preview" = "Componentes de icono en una cuadrícula de 24 px con líneas guía y una vista de tamaños"
|
||||
"Icon set starter" = "Base de conjunto de iconos"
|
||||
"If" = "Si"
|
||||
"If {condition}" = "Si {condition}"
|
||||
"Ignore auto layout (absolute position)" = "Ignorar diseño automático (posición absoluta)"
|
||||
|
|
@ -632,6 +646,8 @@ status = "draft — needs native review"
|
|||
"Key/Gamepad" = "Tecla/Mando"
|
||||
"Key/Gamepad {key}" = "Tecla/Mando {key}"
|
||||
"Keyboard shortcuts" = "Atajos de teclado"
|
||||
"Kits" = "Kits"
|
||||
"Landing page" = "Página de inicio"
|
||||
"Language" = "Idioma"
|
||||
"Lasso" = "Lazo"
|
||||
"Last" = "Último"
|
||||
|
|
@ -675,6 +691,7 @@ status = "draft — needs native review"
|
|||
"Link" = "Enlace"
|
||||
"Link copied" = "Enlace copiado"
|
||||
"Link docs, code or tickets for developers." = "Enlaza documentación, código o tickets para desarrolladores."
|
||||
"Link frames on the Prototype tab, then present the flow" = "Enlaza marcos en la pestaña Prototipo y presenta el flujo"
|
||||
"Link this component to its code component: instances then show your real snippet with their values." = "Vincula este componente a su componente de código: así las instancias muestran tu fragmento real con sus valores."
|
||||
"List spacing" = "Espaciado de lista"
|
||||
"List style" = "Estilo de lista"
|
||||
|
|
@ -692,6 +709,7 @@ status = "draft — needs native review"
|
|||
"Main" = "Principal"
|
||||
"Main component deleted" = "Componente principal eliminado"
|
||||
"Main menu" = "Menú principal"
|
||||
"Make a component" = "Crea un componente"
|
||||
"Make slot" = "Convertir en ranura"
|
||||
"Margin" = "Margen"
|
||||
"Mark all as read" = "Marcar todo como leído"
|
||||
|
|
@ -726,6 +744,7 @@ status = "draft — needs native review"
|
|||
"Mixed" = "Mixto"
|
||||
"Mixed fonts" = "Fuentes mixtas"
|
||||
"Mixed interactions: click + to replace them" = "Interacciones mixtas: haz clic en + para reemplazarlas"
|
||||
"Mobile app" = "Aplicación móvil"
|
||||
"Mode" = "Modo"
|
||||
"Model" = "Modelo"
|
||||
"Models" = "Modelos"
|
||||
|
|
@ -797,6 +816,7 @@ status = "draft — needs native review"
|
|||
"No string variables yet. Create them in Local variables." = "Aún no hay variables de cadena. Créalas en Variables locales."
|
||||
"No styles yet." = "Aún no hay estilos."
|
||||
"No styles yet. Click + to create one from the selection." = "Aún no hay estilos. Haz clic en + para crear uno a partir de la selección."
|
||||
"No templates match your search" = "Ninguna plantilla coincide con tu búsqueda"
|
||||
"No text styles yet. Click + to create one from the selected text." = "Aún no hay estilos de texto. Haz clic en + para crear uno a partir del texto seleccionado."
|
||||
"No variables fit here." = "Ninguna variable encaja aquí."
|
||||
"No variables yet. Create them in Local variables." = "Aún no hay variables. Créalas en Variables locales."
|
||||
|
|
@ -831,6 +851,7 @@ status = "draft — needs native review"
|
|||
"On click" = "Al hacer clic"
|
||||
"On drag" = "Al arrastrar"
|
||||
"On this page" = "En esta página"
|
||||
"One announcement in five common sizes: square, portrait, story, link and banner" = "Un anuncio en cinco tamaños habituales: cuadrado, vertical, historia, enlace y banner"
|
||||
"Online" = "En línea"
|
||||
"Online files" = "Archivos en línea"
|
||||
"Online sharing is off: files are shared on the local network only." = "Compartir en línea está desactivado: los archivos solo se comparten en la red local."
|
||||
|
|
@ -903,6 +924,7 @@ status = "draft — needs native review"
|
|||
"Pick color" = "Elegir color"
|
||||
"Pick color (I)" = "Tomar color (I)"
|
||||
"Pick one font family first" = "Elige primero una familia de fuentes"
|
||||
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Elige la herramienta Marco y arrastra en el lienzo o escoge un tamaño de dispositivo"
|
||||
"Pin a property" = "Fijar una propiedad"
|
||||
"Pinch" = "Pellizco"
|
||||
"Pixel grid" = "Cuadrícula de píxeles"
|
||||
|
|
@ -928,6 +950,7 @@ status = "draft — needs native review"
|
|||
"Present or preview" = "Presentar o previsualizar"
|
||||
"Present this flow" = "Presentar este flujo"
|
||||
"Presentation" = "Presentación"
|
||||
"Presentation slides" = "Diapositivas de presentación"
|
||||
"Presenting: comments" = "Presentando: comentarios"
|
||||
"Presenting: cycle the size options" = "Presentando: alternar las opciones de tamaño"
|
||||
"Presenting: full screen" = "Presentando: pantalla completa"
|
||||
|
|
@ -1066,6 +1089,9 @@ status = "draft — needs native review"
|
|||
"Rulers" = "Reglas"
|
||||
"Run" = "Ejecutar"
|
||||
"Run {label}" = "Ejecutar {label}"
|
||||
"Search templates" = "Buscar plantillas"
|
||||
"See all templates" = "Ver todas las plantillas"
|
||||
"Select layers and they space themselves as their content changes" = "Selecciona capas y se espaciarán solas cuando cambie su contenido"
|
||||
"SHA-256: {sha}…" = "SHA-256: {sha}…"
|
||||
"Same as layer" = "Igual que la capa"
|
||||
"Saturation" = "Saturación"
|
||||
|
|
@ -1125,6 +1151,7 @@ status = "draft — needs native review"
|
|||
"Shape tools" = "Herramientas de forma"
|
||||
"Share" = "Compartir"
|
||||
"Share this file" = "Compartir este archivo"
|
||||
"Share with a code" = "Comparte con un código"
|
||||
"Share: live session, export" = "Compartir: sesión en vivo, exportar"
|
||||
"Shared" = "Compartido"
|
||||
"Shared: copy its code from the file's menu" = "Compartido: copia su código desde el menú del archivo"
|
||||
|
|
@ -1134,6 +1161,7 @@ status = "draft — needs native review"
|
|||
"Sharing online · {people}" = "Compartiendo en línea · {people}"
|
||||
"Sharing stopped: the file stays on the server, open only to you" = "Se dejó de compartir: el archivo se queda en el servidor, abierto solo para ti"
|
||||
"Show" = "Mostrar"
|
||||
"Show all templates" = "Mostrar todas las plantillas"
|
||||
"Show as grid" = "Mostrar como cuadrícula"
|
||||
"Show as list" = "Mostrar como lista"
|
||||
"Show autosaves" = "Mostrar autoguardados"
|
||||
|
|
@ -1166,7 +1194,9 @@ status = "draft — needs native review"
|
|||
"Small caps" = "Versalitas"
|
||||
"Smart animate" = "Animación inteligente"
|
||||
"Snap to pixel grid" = "Ajustar a la cuadrícula de píxeles"
|
||||
"Social and slides" = "Redes y diapositivas"
|
||||
"Social media" = "Redes sociales"
|
||||
"Social post sizes" = "Tamaños para redes sociales"
|
||||
"Soft light" = "Luz suave"
|
||||
"Solid" = "Sólido"
|
||||
"Some variants have the same property values: give each one a unique combination" = "Algunas variantes tienen los mismos valores de propiedad: da a cada una una combinación única"
|
||||
|
|
@ -1191,9 +1221,11 @@ status = "draft — needs native review"
|
|||
"Start" = "Inicio"
|
||||
"Start ascending from / stop descending at" = "Contar hacia arriba desde / hacia abajo hasta"
|
||||
"Start from a blank canvas" = "Empieza con un lienzo en blanco"
|
||||
"Start from a template" = "Empieza con una plantilla"
|
||||
"Start on path" = "Inicio en el trazado"
|
||||
"Start point" = "Punto inicial"
|
||||
"Start sharing" = "Empezar a compartir"
|
||||
"Start sharing with the Share button, then pass on the code it gives" = "Empieza a compartir con el botón Compartir y pasa el código que te da"
|
||||
"Starting to share…" = "Empezando a compartir…"
|
||||
"Status" = "Estado"
|
||||
"Sticky" = "Adherido"
|
||||
|
|
@ -1242,6 +1274,7 @@ status = "draft — needs native review"
|
|||
"Tell the server's operator what is wrong with it" = "Explica al operador del servidor qué tiene de malo"
|
||||
"Temperature" = "Temperatura"
|
||||
"Template" = "Plantilla"
|
||||
"Templates" = "Plantillas"
|
||||
"Terms" = "Condiciones"
|
||||
"Text" = "Texto"
|
||||
"Text align" = "Alineación del texto"
|
||||
|
|
@ -1299,6 +1332,8 @@ status = "draft — needs native review"
|
|||
"Truncate with an ellipsis" = "Truncar con puntos suspensivos"
|
||||
"Try again" = "Reintentar"
|
||||
"Try another search or filter." = "Prueba otra búsqueda u otro filtro."
|
||||
"Try another word, or look at every kind." = "Prueba con otra palabra o mira todos los tipos."
|
||||
"Turn a layer into a component, reuse it, and change every copy at once" = "Convierte una capa en componente, reutilízalo y cambia todas las copias a la vez"
|
||||
"Two digits" = "Dos dígitos"
|
||||
"Type" = "Tipo"
|
||||
"Type settings" = "Ajustes de texto"
|
||||
|
|
@ -1369,6 +1404,7 @@ status = "draft — needs native review"
|
|||
"Watch" = "Reloj"
|
||||
"Wavy" = "Ondulado"
|
||||
"Web" = "Web"
|
||||
"Websites" = "Sitios web"
|
||||
"WebSocket URL" = "URL de WebSocket"
|
||||
"WebSocket URL copied" = "URL de WebSocket copiada"
|
||||
"Wedge" = "Cuña"
|
||||
|
|
@ -1386,6 +1422,7 @@ status = "draft — needs native review"
|
|||
"Width" = "Ancho"
|
||||
"Width point" = "Punto de ancho"
|
||||
"Width profile" = "Perfil de ancho"
|
||||
"Wireframe kit" = "Kit de wireframes"
|
||||
"With the layers' export settings" = "Con los ajustes de exportación de las capas"
|
||||
"Wrap" = "Ajustar"
|
||||
"Wrap in new section" = "Envolver en una sección nueva"
|
||||
|
|
@ -1661,6 +1698,7 @@ status = "draft — needs native review"
|
|||
"file.exportSvg" = "Exportar selección como SVG"
|
||||
"file.import" = "Importar"
|
||||
"file.new" = "Nuevo archivo de diseño"
|
||||
"file.newFromTemplate" = "Nuevo desde plantilla"
|
||||
"file.open" = "Abrir"
|
||||
"file.openRecent" = "Abrir reciente"
|
||||
"file.placeImage" = "Colocar imagen"
|
||||
|
|
@ -1687,6 +1725,7 @@ status = "draft — needs native review"
|
|||
"guide.move" = "Mover guía"
|
||||
"guide.remove" = "Quitar guía"
|
||||
"guide.select" = "Seleccionar guía"
|
||||
"help.gettingStarted" = "Primeros pasos"
|
||||
"help.about" = "Acerca de FigCraft"
|
||||
"help.page" = "Página de ayuda"
|
||||
"help.shortcuts" = "Atajos de teclado"
|
||||
|
|
@ -2064,6 +2103,7 @@ status = "draft — needs native review"
|
|||
"widget.list" = "Listar widgets"
|
||||
"window.focus" = "Traer la ventana al frente"
|
||||
"window.new" = "Nueva ventana"
|
||||
"template.list" = "Listar plantillas"
|
||||
|
||||
[menu]
|
||||
"bool.exclude" = "Excluir"
|
||||
|
|
|
|||
|
|
@ -257,7 +257,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "Commentaire"
|
||||
"Comments" = "Commentaires"
|
||||
"Commits" = "Commits"
|
||||
"Community" = "Communauté"
|
||||
"Company" = "Entreprise"
|
||||
"Compare changes" = "Comparer les modifications"
|
||||
"Completed" = "Terminé"
|
||||
|
|
|
|||
|
|
@ -259,7 +259,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "コメント"
|
||||
"Comments" = "コメント"
|
||||
"Commits" = "コミット"
|
||||
"Community" = "コミュニティ"
|
||||
"Company" = "会社"
|
||||
"Compare changes" = "変更を比較"
|
||||
"Completed" = "完了"
|
||||
|
|
|
|||
|
|
@ -258,7 +258,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "댓글"
|
||||
"Comments" = "댓글"
|
||||
"Commits" = "커밋"
|
||||
"Community" = "커뮤니티"
|
||||
"Company" = "회사"
|
||||
"Compare changes" = "변경 사항 비교"
|
||||
"Completed" = "완료됨"
|
||||
|
|
|
|||
|
|
@ -256,7 +256,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "Comentário"
|
||||
"Comments" = "Comentários"
|
||||
"Commits" = "Commits"
|
||||
"Community" = "Comunidade"
|
||||
"Company" = "Empresa"
|
||||
"Compare changes" = "Comparar alterações"
|
||||
"Completed" = "Concluído"
|
||||
|
|
|
|||
|
|
@ -259,7 +259,6 @@ status = "draft — needs native review"
|
|||
"Comment" = "评论"
|
||||
"Comments" = "评论"
|
||||
"Commits" = "提交"
|
||||
"Community" = "社区"
|
||||
"Company" = "公司"
|
||||
"Compare changes" = "比较更改"
|
||||
"Completed" = "已完成"
|
||||
|
|
|
|||
|
|
@ -344,8 +344,8 @@ pub fn handle(app: &mut FigApp, ctx: &Context, method: &str, p: &Value) -> Resul
|
|||
if let Some(h) = p.get("home").and_then(Value::as_bool) {
|
||||
app.ui.home = h;
|
||||
}
|
||||
if let Some(s) = p.get("homeSection").and_then(Value::as_str) {
|
||||
app.ui.online.open = s == "online";
|
||||
if let Some(section) = p.get("homeSection").and_then(Value::as_str).and_then(crate::home::Section::parse) {
|
||||
app.ui.home_view.section = section;
|
||||
}
|
||||
if let Some(h) = p.get("palette").and_then(Value::as_bool) {
|
||||
app.ui.palette = if h { Some(String::new()) } else { None };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
//! The home screen: new file, open, join with a code, recent files, and your online files.
|
||||
//! The home screen: Recents (new file, open, join with a code, the Getting started tips, open and
|
||||
//! recent files, and templates to start from on a first run), Online (your online files:
|
||||
//! [`crate::online`]) and Templates (the starter templates as cards with thumbnails rendered from
|
||||
//! the documents themselves, searched and filtered by kind).
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Id, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use figcraft_engine::templates::{Kind, TEMPLATES, Template};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
|
|
@ -10,6 +16,64 @@ use crate::icons::{self, Icon};
|
|||
use crate::theme::{ACCENT, Tokens, regular, semibold};
|
||||
use crate::widgets;
|
||||
|
||||
/// The section the home screen shows.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Section {
|
||||
#[default]
|
||||
Recents,
|
||||
Online,
|
||||
Templates,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
/// The sidebar, top to bottom.
|
||||
const ALL: [(Section, Icon, &'static str); 3] = [
|
||||
(Section::Recents, Icon::Home, tr_noop("Recents")),
|
||||
(Section::Online, Icon::Online, tr_noop("Online")),
|
||||
(Section::Templates, Icon::Assets, tr_noop("Templates")),
|
||||
];
|
||||
|
||||
/// The id `ui.set {homeSection}` and `ui.inspect` use.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Section::Recents => "recents",
|
||||
Section::Online => "online",
|
||||
Section::Templates => "templates",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(id: &str) -> Option<Section> {
|
||||
Section::ALL.into_iter().map(|(section, ..)| section).find(|section| section.id() == id)
|
||||
}
|
||||
}
|
||||
|
||||
/// The home screen's own state.
|
||||
#[derive(Default)]
|
||||
pub struct Home {
|
||||
pub section: Section,
|
||||
/// The Templates search text and kind filter.
|
||||
pub query: String,
|
||||
pub kind: Option<Kind>,
|
||||
}
|
||||
|
||||
const SIDEBAR: f32 = 240.0;
|
||||
const CARD_W: f32 = 248.0;
|
||||
const THUMB_H: f32 = 148.0;
|
||||
const CARD_H: f32 = THUMB_H + 86.0;
|
||||
const GAP: f32 = 20.0;
|
||||
/// Templates offered on a first run.
|
||||
const FIRST_RUN: usize = 4;
|
||||
|
||||
/// The Getting started tips: (title, what to do, the command whose shortcut shows).
|
||||
const TIPS: [(&str, &str, &str); 6] = [
|
||||
(tr_noop("Draw a frame"), tr_noop("Pick the Frame tool, then drag on the canvas or choose a device size"), "tool.frame"),
|
||||
(tr_noop("Add auto layout"), tr_noop("Select layers and they space themselves as their content changes"), "layout.add"),
|
||||
(tr_noop("Make a component"), tr_noop("Turn a layer into a component, reuse it, and change every copy at once"), "component.create"),
|
||||
(tr_noop("Prototype"), tr_noop("Link frames on the Prototype tab, then present the flow"), "proto.present"),
|
||||
(tr_noop("Share with a code"), tr_noop("Start sharing with the Share button, then pass on the code it gives"), "collab.share"),
|
||||
(tr_noop("Export"), tr_noop("Export frames and layers as PNG, JPG, SVG or PDF"), "file.export"),
|
||||
];
|
||||
|
||||
/// A row of the home screen's file lists.
|
||||
pub(crate) struct FileRow<'a> {
|
||||
/// The mark before the name, and its colour.
|
||||
|
|
@ -35,153 +99,185 @@ pub(crate) fn file_row(ui: &Ui, t: &Tokens, r: Rect, resp: &Response, row: &File
|
|||
ui.painter().text(pos2(r.min.x + 32.0, r.center().y), Align2::LEFT_CENTER, name, regular(12.0), row.ink);
|
||||
}
|
||||
|
||||
/// A row of Recents' own lists: laid out in place, named for screen readers, ringed with the
|
||||
/// keyboard on it; true when chosen.
|
||||
fn listed(ui: &mut Ui, t: &Tokens, id: Id, height: f32, row: &FileRow) -> bool {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width().min(640.0), height), Sense::hover());
|
||||
let resp = ui.interact(r, id, Sense::click());
|
||||
a11y::button(&resp, row.name);
|
||||
file_row(ui, t, r, &resp, row);
|
||||
focus_ring(ui, t, r, &resp, 5);
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| {
|
||||
let rect = ui.max_rect();
|
||||
let left = Rect::from_min_size(rect.min, vec2(240.0, rect.height()));
|
||||
let left = Rect::from_min_size(rect.min, vec2(SIDEBAR, rect.height()));
|
||||
ui.painter().line_segment([left.right_top(), left.right_bottom()], Stroke::new(1.0, t.border));
|
||||
// Sidebar.
|
||||
let mut y = left.min.y + 20.0;
|
||||
icons::paint(ui.painter(), Rect::from_min_size(pos2(left.min.x + 16.0, y), vec2(20.0, 20.0)), Icon::Logo, ACCENT);
|
||||
ui.painter().text(pos2(left.min.x + 44.0, y + 10.0), Align2::LEFT_CENTER, "FigCraft", semibold(14.0), t.text);
|
||||
y += 48.0;
|
||||
// Recents and Online are the sections there are; the others are placeholders.
|
||||
for (icon, label, section) in [
|
||||
(Icon::Home, tr_noop("Recents"), Some(false)),
|
||||
(Icon::Online, tr_noop("Online"), Some(true)),
|
||||
(Icon::Page, tr_noop("Drafts"), None),
|
||||
(Icon::Assets, tr_noop("Community"), None),
|
||||
] {
|
||||
let r = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(224.0, 32.0));
|
||||
let selected = section == Some(app.ui.online.open);
|
||||
if let Some(online) = section {
|
||||
let resp = ui.interact(r, ui.id().with(("home_section", label)), Sense::click());
|
||||
a11y::tab(&resp, tr(label), selected);
|
||||
if resp.clicked() {
|
||||
app.ui.online.open = online;
|
||||
}
|
||||
if resp.hovered() && !selected {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
|
||||
}
|
||||
}
|
||||
if selected {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.selected_row);
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_min_size(pos2(r.min.x + 10.0, r.center().y - 8.0), vec2(16.0, 16.0)), icon, t.icon);
|
||||
ui.painter().text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, tr(label), regular(12.0), t.text);
|
||||
y += 34.0;
|
||||
}
|
||||
let main = Rect::from_min_max(pos2(left.max.x + 40.0, rect.min.y + 24.0), rect.max - vec2(40.0, 24.0));
|
||||
if app.ui.online.open {
|
||||
crate::online::section(app, ui, main, &t);
|
||||
sidebar(app, ui, &t, left);
|
||||
if app.ui.home_view.section == Section::Online {
|
||||
// Your online files lay themselves out in the area (and scroll their own list).
|
||||
let area = Rect::from_min_max(pos2(left.max.x + 40.0, rect.min.y + 24.0), rect.max - vec2(40.0, 24.0));
|
||||
crate::online::section(app, ui, area, &t);
|
||||
opened_join(app);
|
||||
return;
|
||||
}
|
||||
ui.painter().text(main.min, Align2::LEFT_TOP, tr("Recents"), semibold(18.0), t.text);
|
||||
// Action cards.
|
||||
let mut x = main.min.x;
|
||||
let cy = main.min.y + 48.0;
|
||||
let mut new_file = false;
|
||||
let mut open = false;
|
||||
for (icon, title, sub) in [
|
||||
(Icon::Plus, tr_noop("New design file"), tr_noop("Start from a blank canvas")),
|
||||
(Icon::Page, tr_noop("Open file"), tr_noop("Open .figcraft, .svg or images")),
|
||||
] {
|
||||
let r = Rect::from_min_size(pos2(x, cy), vec2(260.0, 72.0));
|
||||
let mut resp = ui.interact(r, ui.id().with(("card", title)), Sense::click());
|
||||
// One line each inside the card; a long translation ends with an ellipsis, in full on hover.
|
||||
let text_w = r.width() - 72.0;
|
||||
let (title_shown, sub_shown) = (widgets::elide_font(ui, tr(title), text_w, semibold(12.0)), widgets::elide(ui, tr(sub), text_w));
|
||||
if title_shown != tr(title) || sub_shown != tr(sub) {
|
||||
resp = resp.on_hover_text(format!("{}\n{}", tr(title), tr(sub)));
|
||||
}
|
||||
ui.painter().rect_filled(r, CornerRadius::same(8), if resp.hovered() { t.hover } else { t.panel });
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(8), Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
let ir = Rect::from_min_size(pos2(r.min.x + 16.0, r.center().y - 16.0), vec2(32.0, 32.0));
|
||||
ui.painter().rect_filled(ir, CornerRadius::same(6), ACCENT);
|
||||
icons::paint(ui.painter(), ir.shrink(8.0), icon, egui::Color32::WHITE);
|
||||
ui.painter().text(pos2(r.min.x + 60.0, r.center().y - 8.0), Align2::LEFT_CENTER, title_shown, semibold(12.0), t.text);
|
||||
ui.painter().text(pos2(r.min.x + 60.0, r.center().y + 9.0), Align2::LEFT_CENTER, sub_shown, regular(11.0), t.text_secondary);
|
||||
if resp.clicked() {
|
||||
if title.starts_with("New") {
|
||||
new_file = true;
|
||||
} else {
|
||||
open = true;
|
||||
}
|
||||
}
|
||||
x += 280.0;
|
||||
}
|
||||
if new_file {
|
||||
app.run("file.new", json!({}));
|
||||
app.ui.home = false;
|
||||
}
|
||||
if open {
|
||||
app.open_dialog();
|
||||
}
|
||||
// Join a file someone shared: the join field, and what the join is doing.
|
||||
let join = Rect::from_min_size(pos2(main.min.x, cy + 96.0), vec2(main.width().min(540.0), 74.0));
|
||||
ui.painter().text(join.min, Align2::LEFT_TOP, tr("Join with a code"), semibold(12.0), t.text_secondary);
|
||||
let field = Rect::from_min_max(pos2(join.min.x, join.min.y + 24.0), join.max);
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(field).layout(egui::Layout::top_down(egui::Align::Min)), |ui| {
|
||||
if let Some(session) = crate::live::join_row(app, ui, 260.0) {
|
||||
app.ui.live.home_join = Some(session);
|
||||
}
|
||||
if let Some(session) = app.ui.live.home_join {
|
||||
crate::live::join_status(app, ui, &t, session);
|
||||
}
|
||||
let main = Rect::from_min_max(pos2(left.max.x, rect.min.y), rect.max);
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(main), |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("home").auto_shrink([false, false]).show(ui, |ui| {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::symmetric(40, 24)).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(GAP, 0.0);
|
||||
match app.ui.home_view.section {
|
||||
Section::Templates => templates(app, ui, &t),
|
||||
_ => recents(app, ui, &t),
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
opened_join(app);
|
||||
// Open documents and recent files.
|
||||
let mut y = join.max.y + 22.0;
|
||||
ui.painter().text(pos2(main.min.x, y), Align2::LEFT_TOP, tr("Open files"), semibold(12.0), t.text_secondary);
|
||||
y += 28.0;
|
||||
});
|
||||
}
|
||||
|
||||
fn sidebar(app: &mut FigApp, ui: &mut Ui, t: &Tokens, left: Rect) {
|
||||
let mut y = left.min.y + 20.0;
|
||||
icons::paint(ui.painter(), Rect::from_min_size(pos2(left.min.x + 16.0, y), vec2(20.0, 20.0)), Icon::Logo, ACCENT);
|
||||
ui.painter().text(pos2(left.min.x + 44.0, y + 10.0), Align2::LEFT_CENTER, "FigCraft", semibold(14.0), t.text);
|
||||
y += 48.0;
|
||||
for (section, icon, label) in Section::ALL {
|
||||
let r = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(SIDEBAR - 16.0, 32.0));
|
||||
let resp = ui.interact(r, ui.id().with(("home_section", label)), Sense::click());
|
||||
let current = app.ui.home_view.section == section;
|
||||
a11y::tab(&resp, tr(label), current);
|
||||
if current || resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), if current { t.selected_row } else { t.hover });
|
||||
}
|
||||
focus_ring(ui, t, r, &resp, 5);
|
||||
icons::paint(ui.painter(), Rect::from_min_size(pos2(r.min.x + 10.0, r.center().y - 8.0), vec2(16.0, 16.0)), icon, t.icon);
|
||||
ui.painter().text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, tr(label), regular(12.0), t.text);
|
||||
if resp.clicked() {
|
||||
app.ui.home_view.section = section;
|
||||
}
|
||||
y += 34.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// A ring around a card or row that has the keyboard.
|
||||
fn focus_ring(ui: &Ui, t: &Tokens, rect: Rect, resp: &Response, radius: u8) {
|
||||
if resp.has_focus() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(radius), Stroke::new(2.0, t.focus), StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
|
||||
/// A card's background and border (lit on hover, ringed with the keyboard on it).
|
||||
fn card_frame(ui: &Ui, t: &Tokens, rect: Rect, resp: &Response) {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(8), if resp.hovered() { t.hover } else { t.panel });
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(8), Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
focus_ring(ui, t, rect, resp, 8);
|
||||
}
|
||||
|
||||
fn heading(ui: &mut Ui, t: &Tokens, text: &str) {
|
||||
ui.label(egui::RichText::new(text).font(semibold(18.0)).color(t.text));
|
||||
ui.add_space(24.0);
|
||||
}
|
||||
|
||||
fn subheading(ui: &mut Ui, t: &Tokens, text: &str) {
|
||||
ui.add_space(28.0);
|
||||
ui.label(egui::RichText::new(text).font(semibold(12.0)).color(t.text_secondary));
|
||||
ui.add_space(12.0);
|
||||
}
|
||||
|
||||
/// A card with an icon tile, a title and a line under it; true when chosen.
|
||||
fn action_card(ui: &mut Ui, t: &Tokens, icon: Icon, title: &str, sub: &str) -> bool {
|
||||
let (r, mut resp) = ui.allocate_exact_size(vec2(260.0, 72.0), Sense::click());
|
||||
a11y::button(&resp, title);
|
||||
// One line each inside the card; a long translation ends with an ellipsis, in full on hover.
|
||||
let text_w = r.width() - 72.0;
|
||||
let (title_shown, sub_shown) = (widgets::elide_font(ui, title, text_w, semibold(12.0)), widgets::elide(ui, sub, text_w));
|
||||
if title_shown != title || sub_shown != sub {
|
||||
resp = resp.on_hover_text(format!("{title}\n{sub}"));
|
||||
}
|
||||
card_frame(ui, t, r, &resp);
|
||||
let ir = Rect::from_min_size(pos2(r.min.x + 16.0, r.center().y - 16.0), vec2(32.0, 32.0));
|
||||
ui.painter().rect_filled(ir, CornerRadius::same(6), ACCENT);
|
||||
icons::paint(ui.painter(), ir.shrink(8.0), icon, Color32::WHITE);
|
||||
ui.painter().text(pos2(r.min.x + 60.0, r.center().y - 8.0), Align2::LEFT_CENTER, title_shown, semibold(12.0), t.text);
|
||||
ui.painter().text(pos2(r.min.x + 60.0, r.center().y + 9.0), Align2::LEFT_CENTER, sub_shown, regular(11.0), t.text_secondary);
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
fn recents(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
heading(ui, t, tr("Recents"));
|
||||
let (mut new_file, mut open) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
new_file = action_card(ui, t, Icon::Plus, tr("New design file"), tr("Start from a blank canvas"));
|
||||
open = action_card(ui, t, Icon::Page, tr("Open file"), tr("Open .figcraft, .svg or images"));
|
||||
});
|
||||
if new_file {
|
||||
app.run("file.new", json!({}));
|
||||
app.ui.home = false;
|
||||
}
|
||||
if open {
|
||||
app.open_dialog();
|
||||
}
|
||||
// Join a file someone shared: the join field, and what the join is doing.
|
||||
subheading(ui, t, tr("Join with a code"));
|
||||
ui.scope(|ui| {
|
||||
ui.set_max_width(540.0);
|
||||
if let Some(session) = crate::live::join_row(app, ui, 260.0) {
|
||||
app.ui.live.home_join = Some(session);
|
||||
}
|
||||
if let Some(session) = app.ui.live.home_join {
|
||||
crate::live::join_status(app, ui, t, session);
|
||||
}
|
||||
});
|
||||
opened_join(app);
|
||||
if !app.session.prefs.getting_started_dismissed {
|
||||
ui.add_space(28.0);
|
||||
getting_started(app, ui, t);
|
||||
}
|
||||
if !app.session.docs.is_empty() {
|
||||
subheading(ui, t, tr("Open files"));
|
||||
let mut switch = None;
|
||||
for (i, d) in app.session.docs.iter().enumerate() {
|
||||
let r = Rect::from_min_size(pos2(main.min.x, y), vec2(main.width().min(640.0), 36.0));
|
||||
let resp = ui.interact(r, ui.id().with(("open", d.uid)), Sense::click());
|
||||
let detail = d.path.as_deref().unwrap_or_default();
|
||||
let row =
|
||||
FileRow { mark: Some((Icon::Logo, ACCENT)), name: &d.doc.name, ink: t.text, detail, detail_size: 11.0, detail_ink: t.text_secondary };
|
||||
file_row(ui, &t, r, &resp, &row);
|
||||
if resp.clicked() {
|
||||
if listed(ui, t, ui.id().with(("open", d.uid)), 36.0, &row) {
|
||||
switch = Some(i);
|
||||
}
|
||||
y += 38.0;
|
||||
}
|
||||
if let Some(i) = switch {
|
||||
app.run("doc.switch", json!({"index": i}));
|
||||
app.ui.home = false;
|
||||
}
|
||||
y += 16.0;
|
||||
let recents = app.session.prefs.recent_files.clone();
|
||||
if !recents.is_empty() {
|
||||
ui.painter().text(pos2(main.min.x, y), Align2::LEFT_TOP, tr("Recent files"), semibold(12.0), t.text_secondary);
|
||||
y += 28.0;
|
||||
let mut open_path = None;
|
||||
for p in recents.iter().take(12) {
|
||||
let r = Rect::from_min_size(pos2(main.min.x, y), vec2(main.width().min(640.0), 32.0));
|
||||
let resp = ui.interact(r, ui.id().with(("recent", p.as_str())), Sense::click());
|
||||
let name = std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
|
||||
file_row(
|
||||
ui,
|
||||
&t,
|
||||
r,
|
||||
&resp,
|
||||
&FileRow { mark: None, name: &name, ink: t.text, detail: p, detail_size: 10.0, detail_ink: t.text_tertiary },
|
||||
);
|
||||
if resp.clicked() {
|
||||
open_path = Some(p.clone());
|
||||
}
|
||||
y += 34.0;
|
||||
}
|
||||
if let Some(p) = open_path
|
||||
&& app.run("file.open", json!({"path": p})).is_some()
|
||||
{
|
||||
app.ui.home = false;
|
||||
}
|
||||
}
|
||||
let recents = app.session.prefs.recent_files.clone();
|
||||
if recents.is_empty() {
|
||||
// A first run: nothing to reopen yet, so offer something to start from.
|
||||
subheading(ui, t, tr("Start from a template"));
|
||||
let shown: Vec<&Template> = TEMPLATES.iter().take(FIRST_RUN).collect();
|
||||
template_grid(app, ui, t, &shown);
|
||||
ui.add_space(12.0);
|
||||
if widgets::button(ui, tr("See all templates"), false, true).clicked() {
|
||||
app.ui.home_view.section = Section::Templates;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
subheading(ui, t, tr("Recent files"));
|
||||
let mut open_path = None;
|
||||
for p in recents.iter().take(12) {
|
||||
let name = std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
|
||||
let row = FileRow { mark: None, name: &name, ink: t.text, detail: p, detail_size: 10.0, detail_ink: t.text_tertiary };
|
||||
if listed(ui, t, ui.id().with(("recent", p.as_str())), 32.0, &row) {
|
||||
open_path = Some(p.clone());
|
||||
}
|
||||
}
|
||||
if let Some(p) = open_path
|
||||
&& app.run("file.open", json!({"path": p})).is_some()
|
||||
{
|
||||
app.ui.home = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// The file joined from the home screen (a code, or one of your online files) opened in a tab of
|
||||
|
|
@ -196,3 +292,199 @@ fn opened_join(app: &mut FigApp) {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Getting started card: short tips with their shortcuts as the keymap has them now, and a
|
||||
/// button that puts the card away (Help ▸ Getting started brings it back).
|
||||
fn getting_started(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
let width = ui.available_width().min(3.0 * CARD_W + 2.0 * GAP);
|
||||
let columns = if width >= 720.0 { 3 } else { 1 };
|
||||
let (cell_w, cell_h) = ((width - 32.0) / columns as f32, 62.0);
|
||||
let rows = TIPS.len().div_ceil(columns);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, 52.0 + rows as f32 * cell_h + 8.0), Sense::hover());
|
||||
a11y::container_bounds(ui, rect);
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(8), t.field);
|
||||
ui.painter().text(rect.min + vec2(16.0, 24.0), Align2::LEFT_CENTER, tr("Getting started"), semibold(13.0), t.text);
|
||||
let close = Rect::from_min_size(pos2(rect.max.x - 36.0, rect.min.y + 12.0), vec2(24.0, 24.0));
|
||||
if widgets::icon_button_at(
|
||||
ui,
|
||||
ui.id().with("tips_close"),
|
||||
close,
|
||||
Icon::Close,
|
||||
tr("Hide these tips (Getting started in the Help menu shows them again)"),
|
||||
) {
|
||||
app.run("help.gettingStarted", json!({"show": false}));
|
||||
app.services.save_prefs(&app.session.prefs);
|
||||
}
|
||||
for (i, (title, line, command)) in TIPS.into_iter().enumerate() {
|
||||
let cell = Rect::from_min_size(
|
||||
rect.min + vec2(16.0 + (i % columns) as f32 * cell_w, 48.0 + (i / columns) as f32 * cell_h),
|
||||
vec2(cell_w - 16.0, cell_h),
|
||||
);
|
||||
let badge = Rect::from_min_size(cell.min + vec2(0.0, 2.0), vec2(20.0, 20.0));
|
||||
ui.painter().rect_filled(badge, CornerRadius::same(10), t.panel);
|
||||
ui.painter().text(badge.center(), Align2::CENTER_CENTER, (i + 1).to_string(), semibold(10.0), t.text_secondary);
|
||||
let x = cell.min.x + 30.0;
|
||||
let title_end = ui.painter().text(pos2(x, cell.min.y + 12.0), Align2::LEFT_CENTER, tr(title), semibold(12.0), t.text).max.x;
|
||||
if let Some(key) = shortcut(command) {
|
||||
ui.painter().text(pos2(title_end + 8.0, cell.min.y + 12.0), Align2::LEFT_CENTER, key, regular(11.0), t.text_tertiary);
|
||||
}
|
||||
let galley = lines(ui, tr(line), regular(11.0), t.text_secondary, cell.max.x - x, 2);
|
||||
ui.painter().galley(pos2(x, cell.min.y + 24.0), galley, t.text_secondary);
|
||||
}
|
||||
}
|
||||
|
||||
/// A command's shortcut as this platform writes it, as the keymap has it now.
|
||||
pub(crate) fn shortcut(command: &str) -> Option<String> {
|
||||
crate::shortcuts::shortcut(command).map(|key| figcraft_engine::keymap::display(key, crate::shortcuts::platform()))
|
||||
}
|
||||
|
||||
/// `text` wrapped at `width`, at most `rows` lines (the last one ends with an ellipsis).
|
||||
fn lines(ui: &Ui, text: &str, font: egui::FontId, color: Color32, width: f32, rows: usize) -> std::sync::Arc<egui::Galley> {
|
||||
let mut job = egui::text::LayoutJob::simple(text.to_string(), font, color, width.max(1.0));
|
||||
job.wrap.max_rows = rows;
|
||||
ui.fonts_mut(|f| f.layout_job(job))
|
||||
}
|
||||
|
||||
fn matches(template: &Template, query: &str, kind: Option<Kind>) -> bool {
|
||||
let q = query.trim().to_lowercase();
|
||||
let has = |s: &str| s.to_lowercase().contains(&q);
|
||||
kind.is_none_or(|k| k == template.kind)
|
||||
&& (q.is_empty()
|
||||
|| [template.name, template.description, template.kind.label()].into_iter().any(|s| has(s) || has(tr(s)))
|
||||
|| has(template.size))
|
||||
}
|
||||
|
||||
fn templates(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
heading(ui, t, tr("Templates"));
|
||||
ui.horizontal(|ui| {
|
||||
let search = widgets::text_input(ui, Id::new("home_template_search"), &mut app.ui.home_view.query, 260.0, tr("Search templates"));
|
||||
a11y::text(&search, tr("Search templates"), &app.ui.home_view.query);
|
||||
let labels: Vec<&str> = std::iter::once(tr("All")).chain(Kind::ALL.iter().map(|k| tr(k.label()))).collect();
|
||||
let active = app.ui.home_view.kind.and_then(|k| Kind::ALL.iter().position(|x| *x == k)).map_or(0, |i| i + 1);
|
||||
ui.scope(|ui| {
|
||||
ui.set_max_width((labels.len() as f32 * 124.0).min(ui.available_width()));
|
||||
if let Some(i) = widgets::segmented_text(ui, &labels, active) {
|
||||
app.ui.home_view.kind = i.checked_sub(1).and_then(|i| Kind::ALL.get(i).copied());
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(24.0);
|
||||
let (query, kind) = (app.ui.home_view.query.clone(), app.ui.home_view.kind);
|
||||
let shown: Vec<&Template> = TEMPLATES.iter().filter(|x| matches(x, &query, kind)).collect();
|
||||
if shown.is_empty() {
|
||||
ui.add_space(24.0);
|
||||
ui.label(egui::RichText::new(tr("No templates match your search")).font(semibold(13.0)).color(t.text));
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new(tr("Try another word, or look at every kind.")).font(regular(12.0)).color(t.text_secondary));
|
||||
ui.add_space(14.0);
|
||||
if widgets::button(ui, tr("Show all templates"), false, true).clicked() {
|
||||
app.ui.home_view.query.clear();
|
||||
app.ui.home_view.kind = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
template_grid(app, ui, t, &shown);
|
||||
}
|
||||
|
||||
/// Template cards in as many columns as fit; choosing one opens a new document made from it.
|
||||
fn template_grid(app: &mut FigApp, ui: &mut Ui, t: &Tokens, list: &[&Template]) {
|
||||
let columns = (((ui.available_width() + GAP) / (CARD_W + GAP)).floor() as usize).max(1);
|
||||
let dark = ui.visuals().dark_mode;
|
||||
let mut thumbs: Thumbs = ui.data(|d| d.get_temp(Id::new("template_thumbs"))).unwrap_or_default();
|
||||
let mut chosen = None;
|
||||
let mut rendered = false;
|
||||
for row in list.chunks(columns) {
|
||||
ui.horizontal(|ui| {
|
||||
for template in row {
|
||||
let key = (template.id, dark);
|
||||
if !thumbs.0.contains_key(&key) && ui.is_rect_visible(Rect::from_min_size(ui.cursor().min, vec2(CARD_W, CARD_H))) {
|
||||
if rendered {
|
||||
// One a frame keeps the screen moving; the rest follow.
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
thumbs.0.insert(key, thumbnail(ui.ctx(), template, app.session.new_page_background()));
|
||||
rendered = true;
|
||||
}
|
||||
}
|
||||
if template_card(ui, t, template, thumbs.0.get(&key).and_then(Option::as_ref)) {
|
||||
chosen = Some(*template);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(GAP);
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(Id::new("template_thumbs"), thumbs));
|
||||
if let Some(template) = chosen
|
||||
&& app.run("file.newFromTemplate", json!({"template": template.id, "name": tr(template.name)})).is_some()
|
||||
{
|
||||
app.ui.home = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn template_card(ui: &mut Ui, t: &Tokens, template: &Template, thumb: Option<&Thumb>) -> bool {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(CARD_W, CARD_H), Sense::click());
|
||||
let (name, description) = (tr(template.name), tr(template.description));
|
||||
a11y::button(&resp, &format!("{name}: {description}"));
|
||||
card_frame(ui, t, r, &resp);
|
||||
let picture = Rect::from_min_size(r.min, vec2(CARD_W, THUMB_H)).shrink(1.0);
|
||||
let top = CornerRadius { nw: 7, ne: 7, sw: 0, se: 0 };
|
||||
match thumb {
|
||||
Some(thumb) => {
|
||||
ui.painter().rect_filled(picture, top, thumb.page);
|
||||
let size = thumb.texture.size_vec2();
|
||||
let inner = picture.shrink(10.0);
|
||||
let fit = (inner.width() / size.x).min(inner.height() / size.y);
|
||||
let image = Rect::from_center_size(inner.center(), size * fit);
|
||||
ui.painter().image(thumb.texture.id(), image, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
None => {
|
||||
ui.painter().rect_filled(picture, top, t.field);
|
||||
icons::paint(ui.painter(), Rect::from_center_size(picture.center(), vec2(20.0, 20.0)), Icon::Frame, t.icon_secondary);
|
||||
}
|
||||
}
|
||||
ui.painter().line_segment([picture.left_bottom(), picture.right_bottom()], Stroke::new(1.0, t.border));
|
||||
let (x, w) = (r.min.x + 14.0, CARD_W - 28.0);
|
||||
let y = picture.max.y + 20.0;
|
||||
ui.painter().text(pos2(x, y), Align2::LEFT_CENTER, widgets::elide_font(ui, name, w, semibold(12.0)), semibold(12.0), t.text);
|
||||
let galley = lines(ui, description, regular(11.0), t.text_secondary, w, 2);
|
||||
ui.painter().galley(pos2(x, y + 11.0), galley, t.text_secondary);
|
||||
let tag = format!("{} · {}", tr(template.kind.label()), template.size);
|
||||
ui.painter().text(pos2(x, r.max.y - 15.0), Align2::LEFT_CENTER, widgets::elide_font(ui, &tag, w, regular(10.0)), regular(10.0), t.text_tertiary);
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
/// A template's picture: its first page's frames, and the page colour they sit on.
|
||||
#[derive(Clone)]
|
||||
struct Thumb {
|
||||
texture: egui::TextureHandle,
|
||||
page: Color32,
|
||||
}
|
||||
|
||||
/// Pictures by template and theme (the page colour follows the theme); `None`: nothing to show.
|
||||
#[derive(Clone, Default)]
|
||||
struct Thumbs(BTreeMap<(&'static str, bool), Option<Thumb>>);
|
||||
|
||||
/// Build `template` and render the top of its first page to fit a card.
|
||||
fn thumbnail(ctx: &egui::Context, template: &Template, background: figcraft_color::Color) -> Option<Thumb> {
|
||||
let doc = template.build(background).ok()?;
|
||||
let frames = doc.node(doc.first_page()?)?.children.clone();
|
||||
let bounds = frames.iter().filter_map(|id| figcraft_render::export_bounds(&doc, *id)).reduce(|a, b| a.union(b))?;
|
||||
if !(bounds.width() > 0.0 && bounds.height() > 0.0) {
|
||||
return None;
|
||||
}
|
||||
// A page much taller than a card shows its top, as wide as the card.
|
||||
let (w, h) = (f64::from(CARD_W - 22.0), f64::from(THUMB_H - 22.0));
|
||||
let area = figcraft_geom::Rect::new(bounds.x0, bounds.y0, bounds.x1, bounds.y1.min(bounds.y0 + bounds.width() * h / w * 1.5));
|
||||
let scale = (w / area.width()).min(h / area.height()) * f64::from(ctx.pixels_per_point());
|
||||
let pix = figcraft_render::render_area(&doc, &frames, area, scale.min(1.0))?;
|
||||
let image = crate::render_worker::screen_image(&pix, doc.color_profile);
|
||||
let texture = ctx.load_texture(format!("template-thumb-{}", template.id), image, egui::TextureOptions::LINEAR);
|
||||
Some(Thumb { texture, page: crate::theme::c32(background) })
|
||||
}
|
||||
|
||||
/// Tests: the templates whose pictures are cached for the light or dark theme.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn cached_thumbnails(ctx: &egui::Context, dark: bool) -> Vec<&'static str> {
|
||||
let thumbs: Thumbs = ctx.data(|d| d.get_temp(Id::new("template_thumbs"))).unwrap_or_default();
|
||||
thumbs.0.iter().filter(|((_, d), thumb)| *d == dark && thumb.is_some()).map(|((id, _), _)| *id).collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,282 @@
|
|||
//! The home screen, drawn headless: the Templates grid (opened by click and by keyboard, searched,
|
||||
//! filtered, its empty state, its names for screen readers, thumbnails cached per theme and no
|
||||
//! frames once they are in), the first-run row (only without recent files) and the Getting
|
||||
//! started card (closed, kept closed, and brought back from the Help menu).
|
||||
|
||||
use egui::accesskit::Role;
|
||||
use egui::{Event, Key, Modifiers, PointerButton, Pos2, Shape};
|
||||
use figcraft_engine::Session;
|
||||
use figcraft_engine::templates::TEMPLATES;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::home::{Section, cached_thumbnails};
|
||||
use crate::layers_drag_tests::Ui;
|
||||
use crate::live_tests::{flat, has_text, text_center};
|
||||
|
||||
/// The app as a first run finds it: no documents, no recent files.
|
||||
fn first_run() -> Ui {
|
||||
Ui::new(Session::empty())
|
||||
}
|
||||
|
||||
fn shapes(u: &mut Ui) -> Vec<Shape> {
|
||||
flat(&u.frame_output(vec![]).shapes)
|
||||
}
|
||||
|
||||
/// Enough frames for every thumbnail in view (they render one a frame) and what follows a click.
|
||||
fn settle(u: &mut Ui) -> Vec<Shape> {
|
||||
for _ in 0..12 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
shapes(u)
|
||||
}
|
||||
|
||||
fn click(u: &mut Ui, p: Pos2) {
|
||||
for pressed in [true, false] {
|
||||
u.frame(vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
|
||||
}
|
||||
u.frame(vec![]);
|
||||
}
|
||||
|
||||
fn press(u: &mut Ui, key: Key) {
|
||||
for pressed in [true, false] {
|
||||
u.frame(vec![Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }]);
|
||||
}
|
||||
}
|
||||
|
||||
fn templates_tab(u: &mut Ui) -> Vec<Shape> {
|
||||
let s = shapes(u);
|
||||
click(u, text_center(&s, "Templates"));
|
||||
assert_eq!(u.app.ui.home_view.section, Section::Templates);
|
||||
settle(u)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sidebar_has_recents_online_and_templates_and_no_placeholders() {
|
||||
let mut u = first_run();
|
||||
let s = shapes(&mut u);
|
||||
assert!(has_text(&s, "Recents") && has_text(&s, "Online") && has_text(&s, "Templates"));
|
||||
assert!(!has_text(&s, "Drafts") && !has_text(&s, "Community"));
|
||||
u.ctx.enable_accesskit();
|
||||
u.frame(vec![]);
|
||||
let nodes: Vec<_> = u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes.into_iter().map(|(_, n)| n).collect();
|
||||
let tab = |label: &str| nodes.iter().find(|n| n.role() == Role::Tab && n.label() == Some(label)).unwrap_or_else(|| panic!("no tab {label}"));
|
||||
assert_eq!((tab("Recents").is_selected(), tab("Online").is_selected(), tab("Templates").is_selected()), (Some(true), Some(false), Some(false)));
|
||||
// In reading order: Recents, Online, Templates.
|
||||
let top = |label: &str| tab(label).bounds().unwrap().y0;
|
||||
assert!(top("Recents") < top("Online") && top("Online") < top("Templates"));
|
||||
// Each is clicked into, and `ui.inspect` and `ui.set {homeSection}` name them the same way.
|
||||
for (label, section) in [("Online", Section::Online), ("Templates", Section::Templates), ("Recents", Section::Recents)] {
|
||||
let s = shapes(&mut u);
|
||||
click(&mut u, text_center(&s, label));
|
||||
assert_eq!(u.app.ui.home_view.section, section);
|
||||
let inspect = u.app.inspect();
|
||||
assert_eq!(inspect["homeSection"], section.id());
|
||||
assert_eq!(inspect["online"]["open"], section == Section::Online);
|
||||
assert_eq!(Section::parse(section.id()), Some(section));
|
||||
}
|
||||
assert_eq!(Section::parse("drafts"), None);
|
||||
// The Online section is the online files' own.
|
||||
u.app.ui.home_view.section = Section::Online;
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Online files") && !has_text(&s, "Getting started") && !has_text(&s, "Start from a template"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_templates_grid_shows_every_template_and_a_click_opens_a_new_document() {
|
||||
let mut u = first_run();
|
||||
let s = templates_tab(&mut u);
|
||||
for t in TEMPLATES {
|
||||
assert!(has_text(&s, t.name), "{}", t.name);
|
||||
}
|
||||
assert!(has_text(&s, "Apps · 390 × 844"), "the kind and size tag");
|
||||
assert_eq!(cached_thumbnails(&u.ctx, false).len(), TEMPLATES.len(), "every card has its picture");
|
||||
click(&mut u, text_center(&s, "Dashboard"));
|
||||
assert_eq!(u.app.session.docs.len(), 1);
|
||||
let ds = u.app.session.ds().unwrap();
|
||||
assert_eq!(ds.doc.name, "Dashboard");
|
||||
assert!(ds.path.is_none() && !ds.is_dirty(), "a new, untitled-on-disk document");
|
||||
assert!(!u.app.ui.home, "and the editor shows it");
|
||||
// Again: another document, the first untouched.
|
||||
let before = u.app.session.docs[0].doc.clone();
|
||||
u.app.ui.home = true;
|
||||
let s = settle(&mut u);
|
||||
// (Its tag: the name is also on the open document's tab now.)
|
||||
click(&mut u, text_center(&s, "Apps · 1440 × 960"));
|
||||
assert_eq!(u.app.session.docs.len(), 2);
|
||||
assert!(std::sync::Arc::ptr_eq(&u.app.session.docs[0].doc, &before));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cards_are_reached_with_tab_named_for_screen_readers_and_opened_with_enter() {
|
||||
let mut u = first_run();
|
||||
templates_tab(&mut u);
|
||||
u.ctx.enable_accesskit();
|
||||
u.frame(vec![]);
|
||||
let nodes: Vec<_> = u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes.into_iter().map(|(_, n)| n).collect();
|
||||
for t in TEMPLATES {
|
||||
let name = format!("{}: {}", t.name, t.description);
|
||||
assert!(nodes.iter().any(|n| n.role() == Role::Button && n.label() == Some(name.as_str())), "no button named {name:?}");
|
||||
}
|
||||
assert!(nodes.iter().any(|n| n.label() == Some("Search templates")));
|
||||
// Tab walks the controls; the first card it reaches opens with Enter.
|
||||
let mut reached = false;
|
||||
for _ in 0..40 {
|
||||
press(&mut u, Key::Tab);
|
||||
if crate::a11y::focused_name(&u.ctx).starts_with("Mobile app: ") {
|
||||
reached = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(reached, "Tab never reached the first card");
|
||||
press(&mut u, Key::Enter);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(u.app.session.doc().unwrap().name, "Mobile app");
|
||||
assert!(!u.app.ui.home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_and_kind_filter_the_grid_and_nothing_found_offers_a_way_back() {
|
||||
let mut u = first_run();
|
||||
templates_tab(&mut u);
|
||||
u.app.ui.home_view.query = "icon".into();
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Icon set starter") && !has_text(&s, "Dashboard"));
|
||||
// Descriptions and kinds match too, whatever the case.
|
||||
u.app.ui.home_view.query = " PROTOTYPE ".into();
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Mobile app") && !has_text(&s, "Landing page"));
|
||||
u.app.ui.home_view.query.clear();
|
||||
let s = settle(&mut u);
|
||||
click(&mut u, text_center(&s, "Websites"));
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Landing page") && !has_text(&s, "Mobile app") && !has_text(&s, "Icon set starter"));
|
||||
u.app.ui.home_view.query = "zzz".into();
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "No templates match your search"));
|
||||
assert!(TEMPLATES.iter().all(|t| !has_text(&s, t.name)));
|
||||
click(&mut u, text_center(&s, "Show all templates"));
|
||||
let s = settle(&mut u);
|
||||
assert!(u.app.ui.home_view.query.is_empty() && u.app.ui.home_view.kind.is_none());
|
||||
assert!(TEMPLATES.iter().all(|t| has_text(&s, t.name)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_in_the_search_field_filters() {
|
||||
let mut u = first_run();
|
||||
let s = templates_tab(&mut u);
|
||||
let field = text_center(&s, "Search templates");
|
||||
click(&mut u, field);
|
||||
u.frame(vec![Event::Text("slides".into())]);
|
||||
let s = settle(&mut u);
|
||||
assert_eq!(u.app.ui.home_view.query, "slides");
|
||||
assert!(has_text(&s, "Presentation slides") && !has_text(&s, "Dashboard"));
|
||||
assert!(u.app.session.docs.is_empty(), "typing opens nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnails_are_cached_per_theme() {
|
||||
let mut u = first_run();
|
||||
templates_tab(&mut u);
|
||||
assert_eq!(cached_thumbnails(&u.ctx, false).len(), TEMPLATES.len());
|
||||
assert!(cached_thumbnails(&u.ctx, true).is_empty());
|
||||
u.app.session.prefs.theme = "dark".into();
|
||||
settle(&mut u);
|
||||
assert_eq!(cached_thumbnails(&u.ctx, true).len(), TEMPLATES.len(), "the dark theme's pictures");
|
||||
assert_eq!(cached_thumbnails(&u.ctx, false).len(), TEMPLATES.len(), "the light ones are kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_home_screen_draws_no_frames_once_its_thumbnails_are_in() {
|
||||
let (mut h, _) = crate::live_tests::Harness::new();
|
||||
h.app.ui.home = true;
|
||||
for tab in [Section::Templates, Section::Online, Section::Recents] {
|
||||
h.app.ui.home_view.section = tab;
|
||||
h.frames(60);
|
||||
let (_, delay) = h.frame(vec![]);
|
||||
assert!(delay >= std::time::Duration::from_secs(1), "{tab:?} asks for a frame in {delay:?}: {:?}", h.ctx.repaint_causes());
|
||||
}
|
||||
assert_eq!(cached_thumbnails(&h.ctx, false).len(), TEMPLATES.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_first_run_offers_templates_on_recents_and_recent_files_replace_them() {
|
||||
let mut u = first_run();
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Start from a template"));
|
||||
let offered = TEMPLATES.iter().filter(|t| has_text(&s, t.name)).count();
|
||||
assert_eq!(offered, 4);
|
||||
assert!(!has_text(&s, "Recent files") && !has_text(&s, "Open files"));
|
||||
click(&mut u, text_center(&s, "See all templates"));
|
||||
assert_eq!(u.app.ui.home_view.section, Section::Templates);
|
||||
// From the row: a click opens it.
|
||||
u.app.ui.home_view.section = Section::Recents;
|
||||
let s = settle(&mut u);
|
||||
click(&mut u, text_center(&s, "Landing page"));
|
||||
assert_eq!(u.app.session.doc().unwrap().name, "Landing page");
|
||||
// With a recent file, the list shows in the row's place.
|
||||
u.app.ui.home = true;
|
||||
u.app.session.prefs.recent_files = vec!["/tmp/ridgeline.figcraft".into()];
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Recent files") && has_text(&s, "ridgeline.figcraft"));
|
||||
assert!(!has_text(&s, "Start from a template") && !has_text(&s, "See all templates"));
|
||||
assert!(has_text(&s, "Open files"), "the document opened from the template is listed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_getting_started_card_closes_stays_closed_and_comes_back_from_help() {
|
||||
let mut u = first_run();
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Getting started"));
|
||||
for tip in ["Draw a frame", "Add auto layout", "Make a component", "Prototype", "Share with a code", "Export"] {
|
||||
assert!(has_text(&s, tip), "{tip}");
|
||||
}
|
||||
// Shortcuts come from the keymap.
|
||||
let key = crate::home::shortcut("layout.add").unwrap();
|
||||
assert!(has_text(&s, &key), "the auto layout shortcut {key:?}");
|
||||
// The close button is named, and closes the card.
|
||||
u.ctx.enable_accesskit();
|
||||
u.frame(vec![]);
|
||||
let nodes = u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes;
|
||||
let close = nodes.iter().map(|(_, n)| n).find(|n| n.label().is_some_and(|l| l.starts_with("Hide these tips"))).expect("a named close button");
|
||||
let b = close.bounds().unwrap();
|
||||
click(&mut u, Pos2::new(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32));
|
||||
assert!(u.app.session.prefs.getting_started_dismissed);
|
||||
let s = settle(&mut u);
|
||||
assert!(!has_text(&s, "Getting started") && !has_text(&s, "Draw a frame"));
|
||||
assert!(has_text(&s, "Start from a template"), "the rest of the screen stays");
|
||||
// The preference is kept with the others, so it stays closed next time.
|
||||
let kept: figcraft_engine::Prefs = serde_json::from_value(serde_json::to_value(&u.app.session.prefs).unwrap()).unwrap();
|
||||
assert!(kept.getting_started_dismissed);
|
||||
// Help ▸ Getting started: back on the home screen, from wherever.
|
||||
u.app.run("file.new", json!({}));
|
||||
u.app.ui.home = false;
|
||||
u.app.ui.home_view.section = Section::Templates;
|
||||
crate::menus::invoke(&mut u.app, "help.gettingStarted");
|
||||
assert!(u.app.ui.home && u.app.ui.home_view.section == Section::Recents && !u.app.session.prefs.getting_started_dismissed);
|
||||
assert!(has_text(&settle(&mut u), "Getting started"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_file_menu_item_shows_the_templates() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
u.app.ui.home = false;
|
||||
crate::menus::invoke(&mut u.app, "file.newFromTemplate");
|
||||
assert!(u.app.ui.home && u.app.ui.home_view.section == Section::Templates);
|
||||
assert_eq!(u.app.session.docs.len(), 1, "nothing opens until one is chosen");
|
||||
assert!(has_text(&settle(&mut u), "Wireframe kit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_walks_the_sidebar_in_order_before_a_sections_controls() {
|
||||
let mut u = first_run();
|
||||
let mut names = Vec::new();
|
||||
for _ in 0..40 {
|
||||
press(&mut u, Key::Tab);
|
||||
let name = crate::a11y::focused_name(&u.ctx);
|
||||
if !name.is_empty() && names.last() != Some(&name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
let at = |name: &str| names.iter().position(|n| n == name).unwrap_or_else(|| panic!("Tab never reached {name:?}: {names:?}"));
|
||||
assert!(at("Recents") < at("Online") && at("Online") < at("Templates") && at("Templates") < at("Open file"), "{names:?}");
|
||||
}
|
||||
|
|
@ -135,6 +135,8 @@ fn dynamic_labels() -> Vec<&'static str> {
|
|||
v.extend(Group::ALL.iter().map(|g| g.label()));
|
||||
v.extend(GESTURES.iter().flat_map(|(_, label, mac, other)| [*label, *mac, *other]));
|
||||
v.extend(DATA_LABELS);
|
||||
// The starter templates' names, descriptions and kinds.
|
||||
v.extend(figcraft_engine::templates::TEMPLATES.iter().flat_map(|t| [t.name, t.description, t.kind.label()]));
|
||||
v
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ mod field_tests;
|
|||
#[cfg(test)]
|
||||
mod frame_label_tests;
|
||||
#[cfg(test)]
|
||||
mod home_tests;
|
||||
#[cfg(test)]
|
||||
mod idle_tests;
|
||||
#[cfg(test)]
|
||||
mod layers_behaviour_tests;
|
||||
|
|
@ -289,6 +291,8 @@ pub struct UiState {
|
|||
/// An opacity digit just typed and when (a second one soon after makes an exact percentage).
|
||||
pub opacity_digit: Option<(u32, f64)>,
|
||||
pub home: bool,
|
||||
/// The home screen's section, and the Templates search and filter.
|
||||
pub home_view: home::Home,
|
||||
/// How the window's title bar is drawn (the desktop app sets it when it opens the window).
|
||||
pub title_bar: TitleBar,
|
||||
/// Every icon on one screen, for reviewing the set (control channel `ui.set {iconGallery}`).
|
||||
|
|
@ -402,6 +406,7 @@ impl Default for UiState {
|
|||
shortcuts_query: String::new(),
|
||||
opacity_digit: None,
|
||||
home: false,
|
||||
home_view: home::Home::default(),
|
||||
title_bar: TitleBar::Native,
|
||||
icon_gallery: false,
|
||||
toast: None,
|
||||
|
|
@ -657,6 +662,15 @@ impl FigApp {
|
|||
self.ui.libraries = true;
|
||||
}
|
||||
"home" => self.ui.home = true,
|
||||
"home.templates" => {
|
||||
self.ui.home = true;
|
||||
self.ui.home_view.section = home::Section::Templates;
|
||||
}
|
||||
"home.gettingStarted" => {
|
||||
self.ui.home = true;
|
||||
self.ui.home_view.section = home::Section::Recents;
|
||||
self.services.save_prefs(&self.session.prefs);
|
||||
}
|
||||
"dialog.saveAs" => self.save_as_dialog(),
|
||||
"dialog.colorProfile" => self.ui.color_profile = self.session.doc().ok().map(|d| d.color_profile),
|
||||
"present" => {
|
||||
|
|
@ -1116,6 +1130,7 @@ impl FigApp {
|
|||
"home": self.ui.home,
|
||||
"online": online::inspect(self),
|
||||
"tabs": self.session.docs.iter().map(|d| json!({"name": d.doc.name, "live": tabs::live_mark(self, d.uid).map(|shared| if shared { "shared" } else { "private" })})).collect::<Vec<_>>(),
|
||||
"homeSection": self.ui.home_view.section.id(),
|
||||
"dark": self.ui.dark,
|
||||
"focus": {"region": self.ui.nav.region.id(), "keyboard": self.ui.nav.keyboard},
|
||||
"reduceMotion": self.reduce_motion(),
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ const ROW: f32 = 36.0;
|
|||
/// UI-only state of the Online section.
|
||||
#[derive(Default)]
|
||||
pub struct OnlineUi {
|
||||
/// The Online section shows on the home screen (else Recents).
|
||||
pub open: bool,
|
||||
/// The frame the section last drew in: it asks the servers when it shows again after a gap.
|
||||
drawn: Option<u64>,
|
||||
/// Remove from server… was chosen for this file (server, id, name): its confirmation shows.
|
||||
|
|
@ -318,7 +316,7 @@ pub fn inspect(app: &FigApp) -> Value {
|
|||
.map(|f| json!({"file": f.file, "name": f.name, "detail": detail(f), "shared": f.shared.is_some(), "state": f.state, "hasOwnerToken": true}))
|
||||
.collect();
|
||||
json!({
|
||||
"open": app.ui.online.open,
|
||||
"open": app.ui.home_view.section == crate::home::Section::Online,
|
||||
"files": files,
|
||||
"checking": app.session.waiting_for(FILES),
|
||||
"problems": app.session.live.servers.len(),
|
||||
|
|
|
|||
|
|
@ -190,7 +190,12 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
|||
ui.painter().circle_filled(hr.center(), 16.0, t.panel);
|
||||
ui.painter().text(hr.center(), Align2::CENTER_CENTER, "?", crate::theme::regular(15.0), t.text);
|
||||
crate::right::menu_popup(&hresp).show(|ui| {
|
||||
for (l, c) in [(tr("Keyboard shortcuts"), "help.shortcuts"), (tr("Help page"), "help.page"), (tr("About FigCraft"), "help.about")] {
|
||||
for (l, c) in [
|
||||
(tr("Getting started"), "help.gettingStarted"),
|
||||
(tr("Keyboard shortcuts"), "help.shortcuts"),
|
||||
(tr("Help page"), "help.page"),
|
||||
(tr("About FigCraft"), "help.about"),
|
||||
] {
|
||||
if crate::right::menu_item(ui, l, crate::shortcuts::shortcut(c).unwrap_or("")) {
|
||||
app.run(c, json!({}));
|
||||
ui.close();
|
||||
|
|
|
|||
Loading…
Reference in New Issue