go-flutter icon indicating copy to clipboard operation
go-flutter copied to clipboard

Light/dark theme selection

Open geoah opened this issue 5 years ago • 5 comments

Flutter on IOS/Android/MacOS (at least) seems to be have the ability to automatically select between the light/dark themes of a MaterialApp based on system settings.

Running the same app with hover doesn't behave the same way and MediaQuery.platformBrightnessOf(context) always reports Brightness.light which I assume is the default.

Is this something that could be implemented on either go-flutter or as a plugin?

geoah avatar Mar 05 '20 21:03 geoah

It seems that the embedder controls the Brightness. I tried to test this in go-flutter but did not manage to get it working. I based the implementation of code found in the android embedder here.

Is this something that could be implemented on either go-flutter or as a plugin?

Yes, this is implemented by the flutter engine as a native plugin, just like text input.

I'll take a further look at this this week-end.

Here is the go/cmd/options.go file content:

package main

import (
	"encoding/json"
	"fmt"

	"github.com/go-flutter-desktop/go-flutter"
	"github.com/go-flutter-desktop/go-flutter/plugin"
)

var options = []flutter.Option{
	flutter.WindowInitialDimensions(800, 1280),
	flutter.AddPlugin(&SettingsPlugin{}),
}

// SettingsPlugin is a plugin to interact with the internal flutter setting
// system. See https://github.com/flutter/engine/blob/master/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
type SettingsPlugin struct{}

var _ flutter.Plugin = &SettingsPlugin{} // compile-time type check

type settingsJSONMessageCodec struct{} // JSON MessageCodec impl has provided by the plugin writer

// EncodeMessage encodes a settingsJSONMessage to a slice of bytes.
func (j settingsJSONMessageCodec) EncodeMessage(message interface{}) (binaryMessage []byte, err error) {
	return json.Marshal(message)
}

// send-only channel
func (j settingsJSONMessageCodec) DecodeMessage(binaryMessage []byte) (message interface{}, err error) {
	return message, err
}

type settingsJSONMessage struct {
	PlatformBrightness    string  `json:"platformBrightness"`
	AlwaysUse24HourFormat bool    `json:"alwaysUse24HourFormat"`
	TextScaleFactor       float32 `json:"textScaleFactor"`
}

// InitPlugin creates a BasicMessageChannel for "flutter/settings"
func (p *SettingsPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
	channel := plugin.NewBasicMessageChannel(messenger, "flutter/settings", settingsJSONMessageCodec{})

	message := settingsJSONMessage{
		PlatformBrightness:    "dark",
		AlwaysUse24HourFormat: true,
		TextScaleFactor:       1.0,
	}
	err := channel.Send(message)
	if err != nil {
		fmt.Printf("Error sending settings on 'flutter/settings': %v", err)
	}
	return nil
}

pchampio avatar Mar 06 '20 13:03 pchampio

I have updated the above snippet with a working example, sadly, I don't know how to query for dark/light theme on all platform/display environment. As the above settings are quite hard to query on all platform, maybe it's up the the flutter developer to provide the right values.

@GeertJohan @geoah do you thinks those settings (PlatformBrightness, AlwaysUse24HourFormat, TextScaleFactor) should be available through go-flutter options

pchampio avatar Mar 08 '20 17:03 pchampio

@pchampio thanks so much for the update example.


I do agree with the idea of delegating the detection to the developers. The plugin is minimal enough that an example is enough I think

If you'd rather have an option for that maybe the option could accept a channel on which the developer could push updates?

For example macOS can automatically change to dark mode after sundown so applications are expected to dynamically change their light/dark mode. :P I think they are doing this just to annoy devs.


ps. An example of detection on macOS is here: https://gist.github.com/jerblack/869a303d1a604171bf8f00bbbefa59c2#file-2-dark-monitor-go and it also deals with watching for updates.

geoah avatar Mar 08 '20 17:03 geoah

This is literally a 2min mash of @pchampio's example and the gist linked above. Flutter takes a couple more seconds than native flutter but works as expected.

package plugin

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"

	"github.com/go-flutter-desktop/go-flutter"
	"github.com/go-flutter-desktop/go-flutter/plugin"

	"gopkg.in/fsnotify.v1"
)

// SettingsPlugin is a plugin to interact with the internal flutter setting
// system. See https://github.com/flutter/engine/blob/master/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
type SettingsPlugin struct{}

var _ flutter.Plugin = &SettingsPlugin{} // compile-time type check

type settingsJSONMessageCodec struct{} // JSON MessageCodec impl has provided by the plugin writer

// EncodeMessage encodes a settingsJSONMessage to a slice of bytes.
func (j settingsJSONMessageCodec) EncodeMessage(message interface{}) (binaryMessage []byte, err error) {
	return json.Marshal(message)
}

// send-only channel
func (j settingsJSONMessageCodec) DecodeMessage(binaryMessage []byte) (message interface{}, err error) {
	return message, err
}

type settingsJSONMessage struct {
	PlatformBrightness    string  `json:"platformBrightness"`
	AlwaysUse24HourFormat bool    `json:"alwaysUse24HourFormat"`
	TextScaleFactor       float32 `json:"textScaleFactor"`
}

// InitPlugin creates a BasicMessageChannel for "flutter/settings"
func (p *SettingsPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
	channel := plugin.NewBasicMessageChannel(messenger, "flutter/settings", settingsJSONMessageCodec{})

	sendPlatformBrightness := func(isDark bool) {
		platformBrightness := "light"
		if isDark {
			platformBrightness = "dark"
		}
		message := settingsJSONMessage{
			PlatformBrightness:    platformBrightness,
			AlwaysUse24HourFormat: true,
			TextScaleFactor:       1.0,
		}
		err := channel.Send(message)
		if err != nil {
			fmt.Printf("Error sending settings on 'flutter/settings': %v", err)
		}
	}

	sendPlatformBrightness(checkDarkMode())
	go startWatcher(sendPlatformBrightness)

	return nil
}

func checkDarkMode() bool {
	cmd := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle")
	if err := cmd.Run(); err != nil {
		if _, ok := err.(*exec.ExitError); ok {
			return false
		}
	}
	return true
}

const plistPath = `/Library/Preferences/.GlobalPreferences.plist`

var plist = filepath.Join(os.Getenv("HOME"), plistPath)
var wasDark bool

func startWatcher(fn func(bool)) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	done := make(chan bool)
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				if event.Op&fsnotify.Create == fsnotify.Create {
					isDark := checkDarkMode()
					if isDark && !wasDark {
						fn(isDark)
						wasDark = isDark
					}
					if !isDark && wasDark {
						fn(isDark)
						wasDark = isDark
					}
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("error:", err)
			}
		}
	}()

	err = watcher.Add(plist)
	if err != nil {
		log.Fatal(err)
	}
	<-done
}

geoah avatar Mar 08 '20 17:03 geoah

A snippet for windows: https://gist.github.com/jerblack/1d05bbcebb50ad55c312e4d7cf1bc909

I guess detection on linux is going to be a lot harder

provokateurin avatar Mar 08 '20 18:03 provokateurin