Merge branch 'main' into pierremtb/issue2805

This commit is contained in:
Pierre Jacquier
2024-07-12 08:15:41 -04:00
27 changed files with 200 additions and 120 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.24.0", "version": "0.24.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.17.0",

View File

@ -93,23 +93,10 @@ export class LanguageServerPlugin implements PluginValue {
private doSemanticTokens: boolean = false private doSemanticTokens: boolean = false
private doFoldingRanges: boolean = false private doFoldingRanges: boolean = false
private _defferer = deferExecution((code: string) => { // When a doc update needs to be sent to the server, this holds the
try { // timeout handle for it. When null, the server has the up-to-date
// Update the state (not the editor) with the new code. // document.
this.client.textDocumentDidChange({ private sendScheduled: number | null = null
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: code }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}, this.changesDelay)
constructor(options: LanguageServerOptions, private view: EditorView) { constructor(options: LanguageServerOptions, private view: EditorView) {
this.client = options.client this.client = options.client
@ -152,14 +139,9 @@ export class LanguageServerPlugin implements PluginValue {
} }
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
// If the doc didn't change we can return early. if (viewUpdate.docChanged) {
if (!viewUpdate.docChanged) { this.scheduleSendDoc()
return
} }
this.sendChange({
documentText: viewUpdate.state.doc.toString(),
})
} }
destroy() { destroy() {
@ -184,16 +166,6 @@ export class LanguageServerPlugin implements PluginValue {
this.updateFoldingRanges() this.updateFoldingRanges()
} }
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
this._defferer(documentText)
}
requestDiagnostics() {
this.sendChange({ documentText: this.getDocText() })
}
async requestHoverTooltip( async requestHoverTooltip(
view: EditorView, view: EditorView,
{ line, character }: { line: number; character: number } { line, character }: { line: number; character: number }
@ -204,7 +176,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ documentText: this.getDocText() }) this.ensureDocSent()
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
@ -227,6 +199,42 @@ export class LanguageServerPlugin implements PluginValue {
return { pos, end, create: (view) => ({ dom }), above: true } return { pos, end, create: (view) => ({ dom }), above: true }
} }
scheduleSendDoc() {
if (this.sendScheduled != null) window.clearTimeout(this.sendScheduled)
this.sendScheduled = window.setTimeout(
() => this.sendDoc(),
this.changesDelay
)
}
sendDoc() {
if (this.sendScheduled != null) {
window.clearTimeout(this.sendScheduled)
this.sendScheduled = null
}
if (!this.client.ready) return
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.view.state.doc.toString() }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}
ensureDocSent() {
if (this.sendScheduled != null) this.sendDoc()
}
async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> { async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> {
if ( if (
!this.doFoldingRanges || !this.doFoldingRanges ||
@ -284,13 +292,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.client.textDocumentDidChange({ this.ensureDocSent()
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.getDocText() }],
})
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },
@ -330,9 +332,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ this.ensureDocSent()
documentText: context.state.doc.toString(),
})
const result = await this.client.textDocumentCompletion({ const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },

15
src-tauri/Cargo.lock generated
View File

@ -3681,9 +3681,9 @@ dependencies = [
[[package]] [[package]]
name = "num" name = "num"
version = "0.4.3" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-complex", "num-complex",
@ -3695,10 +3695,11 @@ dependencies = [
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [ dependencies = [
"autocfg",
"num-integer", "num-integer",
"num-traits", "num-traits",
] ]
@ -3780,9 +3781,9 @@ dependencies = [
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm", "libm",
@ -3834,7 +3835,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [ dependencies = [
"proc-macro-crate 3.1.0", "proc-macro-crate 1.3.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.70",

View File

@ -80,5 +80,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.24.0" "version": "0.24.1"
} }

View File

@ -39,3 +39,32 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
</AppStateContext.Provider> </AppStateContext.Provider>
) )
} }
interface AppStream {
mediaStream: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
}
const AppStreamContext = createContext<AppStream>({
mediaStream: undefined as unknown as MediaStream,
setMediaStream: () => {},
})
export const useAppStream = () => useContext(AppStreamContext)
export const AppStreamProvider = ({ children }: { children: ReactNode }) => {
const [mediaStream, setMediaStream] = useState<MediaStream>(
undefined as unknown as MediaStream
)
return (
<AppStreamContext.Provider
value={{
mediaStream,
setMediaStream,
}}
>
{children}
</AppStreamContext.Provider>
)
}

View File

@ -114,7 +114,7 @@ export function Toolbar({
() => () =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}), }),
{ enabled: !disableAllButtons, scopes: ['modeling'] } { enabled: !disableAllButtons, scopes: ['modeling'] }
) )
@ -378,7 +378,7 @@ export function Toolbar({
onClick={() => onClick={() =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}) })
} }
disabled={!state.can('Extrude') || disableAllButtons} disabled={!state.can('Extrude') || disableAllButtons}

View File

@ -82,11 +82,11 @@ function ProjectMenuPopover({
}) { }) {
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', ownerMachine: 'modeling' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const findCommand = (obj: { name: string; ownerMachine: string }) => const findCommand = (obj: { name: string; groupId: string }) =>
Boolean( Boolean(
commandBarState.context.commands.find( commandBarState.context.commands.find(
(c) => c.name === obj.name && c.ownerMachine === obj.ownerMachine (c) => c.name === obj.name && c.groupId === obj.groupId
) )
) )

View File

@ -9,6 +9,7 @@ import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { btnName } from 'lib/cameraControls' import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons' import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -17,6 +18,7 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { mediaStream } = useAppStream()
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false) const [isFreezeFrame, setIsFreezeFrame] = useState(false)
@ -124,10 +126,10 @@ export const Stream = () => {
) )
return return
if (!videoRef.current) return if (!videoRef.current) return
if (!context.store?.mediaStream) return if (!mediaStream) return
// Do not immediately play the stream! // Do not immediately play the stream!
videoRef.current.srcObject = context.store.mediaStream videoRef.current.srcObject = mediaStream
videoRef.current.pause() videoRef.current.pause()
send({ send({
@ -136,7 +138,7 @@ export const Stream = () => {
videoElement: videoRef.current, videoElement: videoRef.current,
}, },
}) })
}, [context.store?.mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return if (!isNetworkOkay) return

View File

@ -195,14 +195,15 @@ export class CompletionRequester implements PluginValue {
private queuedUids: string[] = [] private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
this.rejectSuggestionCommand() this.rejectSuggestionCommand()
}, changesDelay) }, changesDelay)
// When a doc update needs to be sent to the server, this holds the
// timeout handle for it. When null, the server has the up-to-date
// document.
private sendScheduledInput: number | null = null
constructor(readonly view: EditorView, client: LanguageServerClient) { constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client this.client = client
} }
@ -245,7 +246,34 @@ export class CompletionRequester implements PluginValue {
} }
this.lastPos = this.view.state.selection.main.head this.lastPos = this.view.state.selection.main.head
if (viewUpdate.docChanged) this._deffererCodeUpdate(true) if (viewUpdate.docChanged) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
this.requestCompletions()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
} }
ghostText(): GhostText | null { ghostText(): GhostText | null {

View File

@ -27,13 +27,10 @@ export class KclPlugin implements PluginValue {
this.client = client this.client = client
} }
private _deffererCodeUpdate = deferExecution(() => { // When a doc update needs to be sent to the server, this holds the
if (this.viewUpdate === null) { // timeout handle for it. When null, the server has the up-to-date
return // document.
} private sendScheduledInput: number | null = null
kclManager.executeCode()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) { if (this.viewUpdate === null) {
@ -101,7 +98,34 @@ export class KclPlugin implements PluginValue {
codeManager.code = newCode codeManager.code = newCode
codeManager.writeToFile() codeManager.writeToFile()
this._deffererCodeUpdate(true) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
kclManager.executeCode()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
} }
async updateUnits( async updateUnits(

View File

@ -4,7 +4,7 @@ import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useAppState } from 'AppState' import { useAppState, useAppStream } from 'AppState'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
@ -28,6 +28,7 @@ export function useSetupEngineManager(
} }
) { ) {
const { setAppState } = useAppState() const { setAppState } = useAppState()
const { setMediaStream } = useAppStream()
const streamWidth = streamRef?.current?.offsetWidth const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight const streamHeight = streamRef?.current?.offsetHeight
@ -54,11 +55,7 @@ export function useSetupEngineManager(
settings.modelingSend settings.modelingSend
) { ) {
engineCommandManager.start({ engineCommandManager.start({
setMediaStream: (mediaStream) => setMediaStream: setMediaStream,
settings.modelingSend({
type: 'Set context',
data: { mediaStream },
}),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }), setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth, width: quadWidth,
height: quadHeight, height: quadHeight,

View File

@ -60,7 +60,8 @@ export default function useStateMachineCommands<
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) => .map((type) =>
createMachineCommand<T, S>({ createMachineCommand<T, S>({
ownerMachine: machineId, // The group is the owner machine's ID.
groupId: machineId,
type, type,
state, state,
send, send,

View File

@ -13,6 +13,7 @@ import {
UpdaterRestartModal, UpdaterRestartModal,
createUpdaterRestartModal, createUpdaterRestartModal,
} from 'components/UpdaterRestartModal' } from 'components/UpdaterRestartModal'
import { AppStreamProvider } from 'AppState'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -26,28 +27,30 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render( root.render(
<HotkeysProvider> <HotkeysProvider>
<Router /> <AppStreamProvider>
<Toaster <Router />
position="bottom-center" <Toaster
toastOptions={{ position="bottom-center"
style: { toastOptions={{
borderRadius: '3px', style: {
}, borderRadius: '3px',
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
success: {
iconTheme: {
primary: 'oklch(89% 0.16 143.4deg)',
secondary: 'oklch(48.62% 0.1654 142.5deg)',
}, },
duration: className:
window?.localStorage.getItem('playwright') === 'true' 'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
? 10 // speed up e2e tests success: {
: 1500, iconTheme: {
}, primary: 'oklch(89% 0.16 143.4deg)',
}} secondary: 'oklch(48.62% 0.1654 142.5deg)',
/> },
<ModalContainer /> duration:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
</AppStreamProvider>
</HotkeysProvider> </HotkeysProvider>
) )

View File

@ -124,7 +124,7 @@ export function createSettingsCommand({
displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), { displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), {
separator: ' ', separator: ' ',
})}`, })}`,
ownerMachine: 'settings', groupId: 'settings',
icon: 'settings', icon: 'settings',
needsReview: false, needsReview: false,
onSubmit: (data) => { onSubmit: (data) => {

View File

@ -65,7 +65,7 @@ export type Command<
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName] CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
> = { > = {
name: CommandName name: CommandName
ownerMachine: T['id'] groupId: T['id']
needsReview: boolean needsReview: boolean
onSubmit: (data?: CommandSchema) => void onSubmit: (data?: CommandSchema) => void
onCancel?: () => void onCancel?: () => void
@ -84,7 +84,7 @@ export type CommandConfig<
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName] CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
> = Omit< > = Omit<
Command<T, CommandName, CommandSchema>, Command<T, CommandName, CommandSchema>,
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview' 'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
> & { > & {
needsReview?: true needsReview?: true
args?: { args?: {

View File

@ -20,7 +20,7 @@ interface CreateMachineCommandProps<
S extends CommandSetSchema<T> S extends CommandSetSchema<T>
> { > {
type: EventFrom<T>['type'] type: EventFrom<T>['type']
ownerMachine: T['id'] groupId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: InterpreterFrom<T>
@ -34,7 +34,7 @@ export function createMachineCommand<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends CommandSetSchema<T>
>({ >({
ownerMachine, groupId,
type, type,
state, state,
send, send,
@ -62,7 +62,7 @@ export function createMachineCommand<
const command: Command<T, typeof type, S[typeof type]> = { const command: Command<T, typeof type, S[typeof type]> = {
name: type, name: type,
ownerMachine: ownerMachine, groupId,
icon, icon,
needsReview: commandConfig.needsReview || false, needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => { onSubmit: (data?: S[typeof type]) => {

View File

@ -57,7 +57,7 @@ export type CommandBarMachineEvent =
} }
| { | {
type: 'Find and select command' type: 'Find and select command'
data: { name: string; ownerMachine: string } data: { name: string; groupId: string }
} }
| { | {
type: 'Change current argument' type: 'Change current argument'
@ -120,9 +120,7 @@ export const commandBarMachine = createMachine(
context.commands.filter( context.commands.filter(
(c) => (c) =>
!event.data.commands.some( !event.data.commands.some(
(c2) => (c2) => c2.name === c.name && c2.groupId === c.groupId
c2.name === c.name &&
c2.ownerMachine === c.ownerMachine
) )
), ),
}), }),
@ -393,9 +391,7 @@ export const commandBarMachine = createMachine(
selectedCommand: (c, e) => { selectedCommand: (c, e) => {
if (e.type !== 'Find and select command') return c.selectedCommand if (e.type !== 'Find and select command') return c.selectedCommand
const found = c.commands.find( const found = c.commands.find(
(cmd) => (cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
cmd.name === e.data.name &&
cmd.ownerMachine === e.data.ownerMachine
) )
return !!found ? found : c.selectedCommand return !!found ? found : c.selectedCommand
@ -514,7 +510,7 @@ export const commandBarMachine = createMachine(
) )
function sortCommands(a: Command, b: Command) { function sortCommands(a: Command, b: Command) {
if (b.ownerMachine === 'auth') return -1 if (b.groupId === 'auth') return -1
if (a.ownerMachine === 'auth') return 1 if (a.groupId === 'auth') return 1
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
} }

View File

@ -138,7 +138,6 @@ export type SegmentOverlayPayload =
} }
interface Store { interface Store {
mediaStream?: MediaStream
videoElement?: HTMLVideoElement videoElement?: HTMLVideoElement
buttonDownInStream: number | undefined buttonDownInStream: number | undefined
didDragInStream: boolean didDragInStream: boolean

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB