diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 847686d..e92721b 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,7 +5,7 @@ on: paths: - "site/**" - "docs/**" - - "packaging/icons/ferese.svg" + - "assets/branding/**" - "scripts/build-site.py" - ".github/workflows/pages.yml" workflow_dispatch: diff --git a/README.md b/README.md index 65e517d..35cd46d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@

- Ferese + + + + Ferese +

A configurable Wayland desktop

diff --git a/assets/branding/ferese-blue-dark.svg b/assets/branding/ferese-blue-dark.svg new file mode 100644 index 0000000..489ca4d --- /dev/null +++ b/assets/branding/ferese-blue-dark.svg @@ -0,0 +1,19 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyMDBiNDZhNS00MzBjLTQ0YzYtOWEyNi00MTJjOTY0ZDZlNTUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaAU3ROHwJLvccKfifOtisiMAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDphYzgzM2JmYS0xNWUxLTQyYjUtYWE0Yy03ZTA4MjIyOTkzN2RscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoKnwqCDusoXBZopMLg7kk1QAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggWrQV4c0Z2wXDwLizvmxmPGGcoXngs0I7I9FDRtxHWrCkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaKsn7yKWhg+JwUsLE4l1HHMAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBdct3O/g97e2fKBP+gUsy4YgkpnwHV1CeZWnYPU69p0mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi5Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjIwMGI0NmE1LTQzMGMtNDRjNi05YTI2LTQxMmM5NjRkNmU1NS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjlkY2FjOTYwLWEzMWUtNDNlMS04YTMxLWJlNTYwN2UxYTI4NHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBatBXhzRnbBcPAuLO+bGY8YZyheeCzQjsj0UNG3EdasKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggxheLqIxsdl4BSsN5iCOfu7njZksjyK9LTM3YVyy3h0uiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggzKu+bf43egViKIdS5Po/09V4cwbp08Z7caz0fE+Woc10Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQLMPG6qaleAEo2sgjt8jBg7FPmIvvWEsLv8nuN6rYv7R4j0ijBj7h/vRNFSmoPdJeY0BhrJhfFioMAXWUKHldBM= + Ferese Blue glow (dark) + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/branding/ferese-blue-flat.svg b/assets/branding/ferese-blue-flat.svg new file mode 100644 index 0000000..aeaaab9 --- /dev/null +++ b/assets/branding/ferese-blue-flat.svg @@ -0,0 +1,15 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo5MjZiMGZjMC03NzgzLTQyMjEtYjQ4ZC1lNmNiYzNjMDViZDIAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaIwcQOifgdYBkQq+CZ9YANQAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo4ODExMjQ0Yy04YjA3LTQ2NzUtODQzMi0zY2NhYzQ0ODViZDFscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo3XpLe6BUgGn6u/EXqBpahwAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggTfOC90ZuSUsMb3m0SS6AHpToOt8ZIwG7pY6E8uPho66kZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaL6emUOsKdDfhUli6KFBL3wAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCHORF8I/qCVx2OCPdNJg7IEQl+IocVqCMSuKCixYOpVmRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi5Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjkyNmIwZmMwLTc3ODMtNDIyMS1iNDhkLWU2Y2JjM2MwNWJkMi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjM2MWY1YThkLTFmNjAtNDhkNC1iMjI5LWI4OWJlYzc3OThhOHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBN84L3Rm5JSwxvebRJLoAelOg63xkjAbuljoTy4+GjrqJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggQ1jsnE3M6NjwOEibVzQLOGVizW8lSiAh24YjmMF/IACiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFgg8+STvA5M2LMsUCIt+QLX2MjHWADRyr/REUb26DN6GaV0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQNuiM2idIjOwTct7+6LBG3TXPZn/iKJ2IkrhT473SS8GDV5d2N7S/cfCIAHiabg22IXFXdkr9zMVMBbgM7wraUg= + Ferese — flat + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/branding/ferese-blue-light.svg b/assets/branding/ferese-blue-light.svg new file mode 100644 index 0000000..c0e59a6 --- /dev/null +++ b/assets/branding/ferese-blue-light.svg @@ -0,0 +1,17 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo0ZGEyNWIzZi00MWY2LTQyZmUtODYxOS1hMzYxZmExZGY5ZjUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaMWT+erUN/0ltovTisUDl3AAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDoxMjlkNWVhYi00ODA1LTRiMGMtYjZlYy0wOGM3NWIyMWE3MDVscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNozOYTMPzLnzA8QdTfbX73ngAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggjlLsRndCO72mRss4tAAhv3x2Pp+nXIUGYhCLkBlNBjWkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaPoH+Alu2rF6Xq43POgXKXwAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCOp0GlGbehYxHafv+VWEY+znuLp7ed4j44iRqLXVOemWRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi5Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjRkYTI1YjNmLTQxZjYtNDJmZS04NjE5LWEzNjFmYTFkZjlmNS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmM0YTIwM2VkLWNmOTMtNDE5NS05NjZlLTMxNDg2NWNlMzlmZXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCOUuxGd0I7vaZGyzi0ACG/fHY+n6dchQZiEIuQGU0GNaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgg16LtIL9sWqVVOUn/rVs9b2vnQJUZvEVx3ijZ9N9AxqOiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggLRwRahdQ3ZbIdkViwGS993ZlrIo3AVfqeDHDQefdmvl0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQN655ttZWCd7S6ee2x/ao8Kcl6/PbEzpfMKvkB1NRcHWzlhFk/qcP5dPJvJyctCvQW4lEr01BIgmxDrPBHOrbsQ= + Ferese Blue (light) + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/wallpapers/ferese-wallpaper-dark.jpg b/assets/wallpapers/ferese-wallpaper-dark.jpg new file mode 100644 index 0000000..ac9c8c8 Binary files /dev/null and b/assets/wallpapers/ferese-wallpaper-dark.jpg differ diff --git a/assets/wallpapers/ferese-wallpaper-light.png b/assets/wallpapers/ferese-wallpaper-light.png new file mode 100644 index 0000000..8835e72 Binary files /dev/null and b/assets/wallpapers/ferese-wallpaper-light.png differ diff --git a/assets/wallpapers/ferese.png b/assets/wallpapers/ferese.png index 7c01d20..d352758 100644 Binary files a/assets/wallpapers/ferese.png and b/assets/wallpapers/ferese.png differ diff --git a/assets/wallpapers/ferese.svg b/assets/wallpapers/ferese.svg deleted file mode 100644 index 7387314..0000000 --- a/assets/wallpapers/ferese.svg +++ /dev/null @@ -1,62 +0,0 @@ -AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo4MzkwZjY3Yi1lMWMzLTRkZjItYjc5Ni1kMThmOTcwMzJiOTgAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaHMKM0mGirl6EAsa915f3d8AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplYTUzOTY3Ni03MTE4LTQzMGItYTU3Ni02MDk2Zjk0ZmY0NTdscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNomXih8e+70OF/YLHlVgL9LgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggnypnxLNZxd05KEbslyHWdZn0du7pvfZdyFM3FXgUiimkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaEYNHg3OE8TdlNzzrWNuGkkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBmyCV0xk4L/Lqf9X1S9qWNTl5TCz9r70H5YR7X/R91+mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBiaZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjgzOTBmNjdiLWUxYzMtNGRmMi1iNzk2LWQxOGY5NzAzMmI5OC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmY4MGM0NTA5LTE1NmEtNDQxYi04ZDU4LTNjMzE5YTRiOWU4ZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCfKmfEs1nF3TkoRuyXIdZ1mfR27um99l3IUzcVeBSKKaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggBkna+FVWA7ttg4e5fN3SGCgCzCQlDA9sXSLaXIILGWiiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggaFremDqsMi+9hC8EXdAqcvUmalJPQpuJro7fci7a9hF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQCF+gEV93krhKWJZo9W7JOAutbiPIwiSmCwzd4sIsW1Qhwbqp1uFgraYgVYRPUkeUEywl5lZ0k47qZOOBpH4jps= - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/compositor/src/wallpaper.rs b/compositor/src/wallpaper.rs index b572623..3f5ed89 100644 --- a/compositor/src/wallpaper.rs +++ b/compositor/src/wallpaper.rs @@ -508,7 +508,7 @@ mod tests { fn default_wallpaper_is_bundled_and_decodable() { let config: WallpaperConfig = ferese_config::from_str("").unwrap(); let path = config.path.unwrap(); - assert_eq!(image::image_dimensions(&path).unwrap(), (1920, 1080)); + assert_eq!(image::image_dimensions(&path).unwrap(), (3840, 2160)); assert!(WallpaperState::new(WallpaperConfig::default()).owns_background()); } } diff --git a/crates/ferese-config/src/lib.rs b/crates/ferese-config/src/lib.rs index b2af2a7..264a3b8 100644 --- a/crates/ferese-config/src/lib.rs +++ b/crates/ferese-config/src/lib.rs @@ -38,13 +38,26 @@ pub fn config_path() -> Option { } pub fn default_wallpaper() -> &'static str { - static PATH: std::sync::OnceLock = std::sync::OnceLock::new(); - PATH.get_or_init(|| { + default_wallpaper_for(theme::Appearance::Dark) +} + +pub fn default_wallpaper_for(appearance: theme::Appearance) -> &'static str { + static LIGHT: std::sync::OnceLock = std::sync::OnceLock::new(); + static DARK: std::sync::OnceLock = std::sync::OnceLock::new(); + let (slot, name) = match appearance { + theme::Appearance::Light => (&LIGHT, "ferese-wallpaper-light.png"), + theme::Appearance::Dark => (&DARK, "ferese-wallpaper-dark.jpg"), + }; + slot.get_or_init(|| { let path = std::env::current_exe() .ok() - .and_then(|path| path.parent().map(|dir| dir.join("wallpapers/ferese.png"))) + .and_then(|path| path.parent().map(|dir| dir.join("wallpapers").join(name))) .filter(|path| path.is_file()) - .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")); + .unwrap_or_else(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../assets/wallpapers") + .join(name) + }); path.canonicalize().unwrap_or(path).to_string_lossy().into_owned() }) diff --git a/crates/ferese-config/src/theme.rs b/crates/ferese-config/src/theme.rs index cb64fff..46d191c 100644 --- a/crates/ferese-config/src/theme.rs +++ b/crates/ferese-config/src/theme.rs @@ -575,6 +575,7 @@ pub fn preset(name: &str, appearance: Appearance) -> Result { return Err(format!("Preset {name} does not support {appearance:?}")); } let mut tokens = default_tokens(); + tokens.background.path = Some(crate::default_wallpaper_for(appearance).into()); tokens.colors.surface_base = preset.base.into(); tokens.colors.surface_raised = preset.raised.into(); tokens.colors.application_background = preset.application_background.into(); @@ -601,6 +602,7 @@ pub fn preset(name: &str, appearance: Appearance) -> Result { return Ok(tokens); } let mut t = default_tokens(); + t.background.path = Some(crate::default_wallpaper_for(appearance).into()); let (base, text, muted, accent, border) = match name { "monochrome" => ("#101012", "#EDEDF0", "#97979F", "#E5E5E5", "#FFFFFF18"), "dracula" => ("#282A36", "#F8F8F2", "#A4ADCD", "#BD93F9", "#44475A"), @@ -962,6 +964,62 @@ mod tests { ) } + #[test] + fn wallpapers_follow_appearance_unless_explicitly_selected() { + for (mode, appearance) in [("light", Appearance::Light), ("dark", Appearance::Dark)] { + let default = candidate(&format!("theme {{ mode \"{mode}\"; }}")).unwrap(); + assert_eq!( + default.theme.tokens.background.path.as_deref(), + Some(Path::new(crate::default_wallpaper_for(appearance))) + ); + + let shared = candidate(&format!( + "theme {{ mode \"{mode}\"; background {{ path \"shared.png\"; }}; }}" + )) + .unwrap(); + assert_eq!( + shared.theme.tokens.background.path.as_deref(), + Some(Path::new("/config/ferese/shared.png")) + ); + + let separate = candidate(&format!("theme {{ mode \"{mode}\"; background {{ path \"shared.png\"; }}; light {{ background {{ path \"light.png\"; }}; }}; dark {{ background {{ path \"dark.jpg\"; }}; }}; }}")).unwrap(); + assert_eq!( + separate.theme.tokens.background.path.as_deref(), + Some(Path::new(&format!( + "/config/ferese/{}", + if appearance == Appearance::Light { + "light.png" + } else { + "dark.jpg" + } + ))) + ); + } + + let auto = candidate("theme { mode \"auto\"; schedule { timezone \"UTC\"; }; }").unwrap(); + assert_eq!(auto.theme.appearance, Appearance::Light); + assert_eq!( + auto.theme.tokens.background.path.as_deref(), + Some(Path::new(crate::default_wallpaper_for(Appearance::Light))) + ); + } + + #[test] + fn imported_theme_wallpaper_is_not_overwritten_by_bundled_defaults() { + let source = "theme { mode \"light\"; family \"custom\"; custom-themes { custom { file \"custom.kdl\"; }; }; }"; + let result = resolve( + &Document::parse(source).unwrap(), + Path::new("/config/ferese"), + "2026-09-30T12:00:00Z".parse().unwrap(), + |_| Ok("light { background { path \"authored.png\"; }; }".into()), + ) + .unwrap(); + assert_eq!( + result.theme.tokens.background.path.as_deref(), + Some(Path::new("/config/ferese/authored.png")) + ); + } + #[test] fn automatic_gradients_follow_presets_and_preserve_border_alpha() { for preset in crate::presets::PRESETS { diff --git a/crates/ferese-theme/assets/icons/ferese.svg b/crates/ferese-theme/assets/icons/ferese.svg index 49117b3..0529370 100644 --- a/crates/ferese-theme/assets/icons/ferese.svg +++ b/crates/ferese-theme/assets/icons/ferese.svg @@ -1,6 +1,6 @@ - Ferese orbit - Open, tilted orbit symbol. Smooth cubic Bezier vector paths; transparent background. + Ferese + Ferese symbol with smooth cubic Bezier paths and a transparent background. diff --git a/crates/ferese-theme/src/gallery.rs b/crates/ferese-theme/src/gallery.rs index ab1e411..a40faeb 100644 --- a/crates/ferese-theme/src/gallery.rs +++ b/crates/ferese-theme/src/gallery.rs @@ -32,11 +32,17 @@ pub fn tile( font, id, } = options; - use cosmic::widget::{button, column, row}; - let mut caption = row([]) - .spacing(6) - .align_y(cosmic::iced::Alignment::Center) - .push(crate::text(family.name.clone(), font).size(12)); + use cosmic::widget::{button, container, row}; + let label_color = family + .palette(variant.unwrap_or(Appearance::Light)) + .or(family.dark.as_ref()) + .map(|palette| crate::parse_color(&palette.text).unwrap()) + .unwrap_or(palette.text); + let mut caption = row([]).spacing(6).align_y(cosmic::iced::Alignment::Center).push( + crate::text(family.name.clone(), font) + .size(12) + .class(cosmic::theme::Text::Color(label_color)), + ); if family.light.is_none() || family.dark.is_none() { caption = caption.push( crate::text( @@ -48,19 +54,31 @@ pub fn tile( font, ) .size(10) - .class(cosmic::theme::Text::Color(palette.muted)), + .class(cosmic::theme::Text::Color(label_color)), ); } - let image = preview(family, variant, active, selected); - let image = - cosmic::widget::responsive(move |size| image.clone().height(Length::Fixed(size.width * 126. / 200.)).into()) - .height(Length::Shrink); - let content = column([]).spacing(2).push(image).push(caption); + let family_preview = family.clone(); + let caption: Element<'static, M> = container(caption) + .padding(8) + .width(Length::Fill) + .height(Length::Fill) + .align_y(cosmic::iced::alignment::Vertical::Bottom) + .into(); + let image = cosmic::widget::responsive(move |size| { + let height = size.width * 126. / 200.; + // Match the button's logical radius when scaling the cached SVG artwork. + let radius = 10. * 200. / size.width.max(1.); + preview_with_radius(&family_preview, variant, active, selected, radius) + .height(Length::Fixed(height)) + .into() + }) + .height(Length::Shrink); + let content = cosmic::iced::widget::stack([image.into(), caption]); let tile = button::custom(content) .id(id.clone()) .name(format!("{}, {}", family.name, family.availability())) .width(Length::Fill) - .padding(8) + .padding(0) .class(tile_style(palette, selected)) .on_press(on_select); radio_with_tab_stop(tile, id, selected, tab_stop, navigate) @@ -70,11 +88,17 @@ fn tile_style(palette: crate::Palette, selected: bool) -> cosmic::theme::Button let style = move |focused: bool, hovered: bool| cosmic::widget::button::Style { shape: Some(BorderShape::Continuous), outline: None, - background: Some(if hovered { palette.sidebar } else { palette.card }.into()), - border_width: if selected { 2. } else { 0. }, + background: None, + border_width: if selected || focused { + 2. + } else if hovered { + 1. + } else { + 0. + }, border_color: palette.accent, border_radius: 10.into(), - outline_width: if focused { 2. } else { 0. }, + outline_width: 0., outline_color: palette.text, text_color: Some(palette.text), icon_color: Some(palette.text), @@ -93,15 +117,26 @@ pub fn preview( variant: Option, active: Option, selected: bool, +) -> cosmic::widget::icon::Icon { + preview_with_radius(family, variant, active, selected, 10.) +} + +fn preview_with_radius( + family: &Family, + variant: Option, + active: Option, + selected: bool, + radius: f32, ) -> cosmic::widget::icon::Icon { use cosmic::widget::icon; - let key = format!("{}/{variant:?}/{active:?}/{selected}", family.id); + let key = format!("{}/{variant:?}/{active:?}/{selected}/{}", family.id, radius.to_bits()); static CACHE: std::sync::OnceLock>> = std::sync::OnceLock::new(); let cache = CACHE.get_or_init(Default::default); let mut cache = cache.lock().unwrap(); if cache.get(&key).is_none_or(|(cached, _)| cached != family) { - let handle = icon::from_svg_bytes(svg(family, variant, active, selected).into_bytes()).symbolic(false); + let handle = icon::from_svg_bytes(svg_with_radius(family, variant, active, selected, radius).into_bytes()) + .symbolic(false); cache.insert(key.clone(), (family.clone(), handle)); } let handle = cache[&key].1.clone(); @@ -130,11 +165,32 @@ fn desktop(p: &Palette) -> String { } pub fn svg(family: &Family, variant: Option, active: Option, selected: bool) -> String { + svg_with_radius(family, variant, active, selected, 10.) +} + +fn svg_with_radius( + family: &Family, + variant: Option, + active: Option, + selected: bool, + radius: f32, +) -> String { let mut result = String::from( r##" -"##, +"##, ); + let outline = + cosmic::iced::border::Outline::new([0., 0., 200., 126.], [f64::from(radius); 4], BorderShape::Continuous) + .expect("finite preview dimensions"); + // Rasterized once per cached preview; no shaped rendering layer per tile. + let points = outline.polygon(0.02).expect("positive contour tolerance"); + result.push_str(""); for appearance in [Appearance::Light, Appearance::Dark] { if variant.is_some_and(|variant| variant != appearance) { continue; @@ -196,7 +252,7 @@ pub fn svg(family: &Family, variant: Option, active: Option"#, p.accent)); } - result.push_str(""); + result.push_str(""); result } diff --git a/docs/configuration.md b/docs/configuration.md index 6a83efe..80a1f81 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -249,7 +249,7 @@ prevent popup cleanup. Settings → Appearance offers six paired themes: **Ferese Blue** (default), **Catppuccin**, **Gruvbox**, **Rosé Pine**, **Tokyo Night**, and **Everforest**. -Ferese Blue uses the logo accent, `#3D7BE6`, and has light and dark variants. +Ferese Blue uses the default accent, `#3D7BE6`, and has light and dark variants. Choose Light, Dark, or Auto in Settings or Control Center. Auto follows local 07:00 and 19:00 boundaries unless you change its schedule. @@ -278,13 +278,34 @@ one appearance. | `appearance.inactive-dim.amount` | number 0–1 | `0.15` | Darkening strength | | `appearance.inactive-dim.duration-ms` | number ≥ 0 | `150` | Dimming transition; 0 snaps | | `theme.typography.font-family` | string | `"Inter"` | Shell, Settings and overview font | -| `theme.background.path` | string | bundled Ferese wallpaper | Wallpaper image path; an existing selection overrides the default | +| `theme.background.path` | string | matching light/dark wallpaper | Shared wallpaper; an explicit path overrides both bundled defaults | +| `theme.light.background.path`, `theme.dark.background.path` | string | shared path, or matching default | Wallpaper for one appearance; overrides the shared path | | `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox | +| `theme.light.background.mode`, `theme.dark.background.mode` | `"fill"`, `"fit"` | shared placement | Image placement for one appearance | | `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material | | `theme.material.opacity` | number 0–1 | `0.78` | Shared shell background opacity for bars, menus, popovers, notifications and themed dialogs. Text/icons stay opaque; 0 hides the material. Solid mode is always opaque | | `theme.material.tint-strength` | number 0–1 | Dark `0.5`, light `1` | Color strength over the blurred backdrop | | `theme.material.blur-radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables | +Settings → Wallpaper shows the current image and both bundled defaults. Use +**Apply to** to change both modes or just one. **Match appearance** restores the +bundled light and dark pair. Custom images can also be selected separately in KDL: + +```kdl +theme { + light { + background { path "~/Pictures/day.png"; } + } + dark { + background { path "~/Pictures/night.jpg"; } + } +} +``` + +The wallpaper follows manual light/dark changes and automatic appearance changes. +A shared `theme.background.path` applies to both modes unless a per-mode path +is set. + | `theme.colors` key | Default | Meaning | | --- | --- | --- | | `surface-base` | `"#111821"` | Material/card base color | diff --git a/docs/images/ferese-lockup.svg b/docs/images/ferese-lockup.svg deleted file mode 100644 index 1f70e2a..0000000 --- a/docs/images/ferese-lockup.svg +++ /dev/null @@ -1,24 +0,0 @@ - - Ferese - The blue Ferese symbol beside the Ferese wordmark - - - - - - - - - - - - - - - Ferese - diff --git a/docs/screenshots.md b/docs/screenshots.md index efea528..15f0852 100644 --- a/docs/screenshots.md +++ b/docs/screenshots.md @@ -4,9 +4,9 @@ These screenshots were taken on September 30, 2026. Click an image to open it at ## Ferese Blue -Settings and Control Center with the default wallpaper. +Settings and Control Center with the previous default wallpaper. -[![Settings and Control Center with the default wallpaper.](images/screenshots/ferese-main-hero.png)](images/screenshots/ferese-main-hero.png) +[![Settings and Control Center with the previous default wallpaper.](images/screenshots/ferese-main-hero.png)](images/screenshots/ferese-main-hero.png) ## Rosé Pine diff --git a/packaging/icons/ferese.svg b/packaging/icons/ferese.svg index 5994494..75d65e2 100644 --- a/packaging/icons/ferese.svg +++ b/packaging/icons/ferese.svg @@ -1,18 +1,18 @@ - Ferese orbit — gradient - Open, tilted orbit symbol. Smooth cubic Bezier vector paths; transparent background. + Ferese — gradient + Ferese symbol with smooth cubic Bezier paths and a transparent background. - + - + - - + + diff --git a/scripts/build-site.py b/scripts/build-site.py index f0c7604..4de7053 100644 --- a/scripts/build-site.py +++ b/scripts/build-site.py @@ -33,7 +33,7 @@ for name in ('index.html', 'styles.css', 'app.js'): (OUTPUT / 'assets').mkdir(exist_ok=True) shutil.copytree(ROOT / 'site/assets', OUTPUT / 'assets', dirs_exist_ok=True) shutil.copytree(ROOT / 'docs/images', OUTPUT / 'assets', dirs_exist_ok=True) -shutil.copy2(ROOT / 'packaging/icons/ferese.svg', OUTPUT / 'assets/ferese.svg') +shutil.copytree(ROOT / 'assets/branding', OUTPUT / 'assets/branding') (OUTPUT / 'docs').mkdir(exist_ok=True) (OUTPUT / '.nojekyll').touch() search_index = [] @@ -103,13 +103,13 @@ for page_number, (slug, title, source) in enumerate(PAGES): doc = f''' {escape(title)} — Ferese handbook - + - +
{body}
View this guide on GitHub
-''' +''' (OUTPUT / 'docs' / f'{slug}.html').write_text(doc) (OUTPUT / 'docs/search-index.json').write_text(json.dumps(search_index, ensure_ascii=False)) print(f'Built website and {len(PAGES)} handbook pages at {OUTPUT}') diff --git a/scripts/installer/install.py b/scripts/installer/install.py index c3922b3..3d2a183 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -143,6 +143,10 @@ def validate_bundle(bundle): for item in manifest['files']: target = item['target'] allowed = expected.get(target) + # Older releases bundled this retired source asset. Keep their manifests + # verifiable for upgrades and rollback without adding it to new bundles. + if allowed is None and target == 'wallpapers/ferese.svg': + allowed = {'target': target, 'mode': '0644'} if allowed is None and re.fullmatch(r'licenses/theme-[A-Za-z0-9._-]+\.txt', target): allowed = {'target': target, 'mode': '0644'} diff --git a/scripts/installer/inventory.json b/scripts/installer/inventory.json index f4d13cc..142ee79 100644 --- a/scripts/installer/inventory.json +++ b/scripts/installer/inventory.json @@ -101,8 +101,13 @@ "mode": "0644" }, { - "source": "assets/wallpapers/ferese.svg", - "target": "wallpapers/ferese.svg", + "source": "assets/wallpapers/ferese-wallpaper-dark.jpg", + "target": "wallpapers/ferese-wallpaper-dark.jpg", + "mode": "0644" + }, + { + "source": "assets/wallpapers/ferese-wallpaper-light.png", + "target": "wallpapers/ferese-wallpaper-light.png", "mode": "0644" }, { diff --git a/scripts/tests/test_portal_install.py b/scripts/tests/test_portal_install.py index a6c7172..1b6a528 100644 --- a/scripts/tests/test_portal_install.py +++ b/scripts/tests/test_portal_install.py @@ -127,6 +127,31 @@ m.main() self.assertEqual(os.readlink(self.base / 'previous'), 'releases/second') self.assertEqual([(self.root / path).read_bytes() for path in (PORTAL, UNIT, DESKTOP)], first) + def test_upgrade_and_rollback_accept_retired_wallpaper_svg_with_verified_checksum(self): + legacy = Path(self.temporary.name) / 'legacy' + shutil.copytree(self.bundles[0], legacy) + svg = legacy / 'wallpapers/ferese.svg' + svg.write_text('\n') + svg.chmod(0o644) + manifest_path = legacy / 'manifest.json' + manifest = json.loads(manifest_path.read_text()) + manifest['files'].append({'target': 'wallpapers/ferese.svg', 'mode': '0644', + 'sha256': installer.digest(svg)}) + manifest_path.write_text(json.dumps(manifest)) + + self.run_command('install', legacy) + self.assertTrue((self.base / 'current/wallpapers/ferese.svg').is_file()) + self.install(1) + self.assertEqual(os.readlink(self.base / 'current'), 'releases/second') + self.assertFalse((self.base / 'current/wallpapers/ferese.svg').exists()) + self.run_command('rollback') + self.assertEqual(os.readlink(self.base / 'current'), 'releases/first') + self.assertEqual((self.base / 'current/wallpapers/ferese.svg').read_bytes(), svg.read_bytes()) + + svg.write_text('corrupt legacy wallpaper') + with self.assertRaisesRegex(ValueError, 'Checksum mismatch: wallpapers/ferese.svg'): + installer.validate_bundle(legacy) + def test_upgrade_uses_previous_installer_ownership_receipts(self): self.install() release = self.base / 'releases/first' diff --git a/shell/ferese-settings/src/connections/controller.rs b/shell/ferese-settings/src/connections/controller.rs index d38ba99..3a22391 100644 --- a/shell/ferese-settings/src/connections/controller.rs +++ b/shell/ferese-settings/src/connections/controller.rs @@ -312,7 +312,7 @@ mod tests { ( "/unused/connections-test.kdl".into(), crate::store::Snapshot::parse(String::new()), - Some(super::super::Tab::Bluetooth), + Some(crate::InitialPage::Connections(super::super::Tab::Bluetooth)), ), ); assert_eq!(app.page, crate::schema::Page::Connections); diff --git a/shell/ferese-settings/src/form_controls.rs b/shell/ferese-settings/src/form_controls.rs index 16f87f8..4e898a2 100644 --- a/shell/ferese-settings/src/form_controls.rs +++ b/shell/ferese-settings/src/form_controls.rs @@ -176,7 +176,7 @@ impl App { .into() } Kind::Choice { default, choices } => { - let value = self.draft.string(&path, default); + let value = self.wallpaper_field_value(&path, default); let mut options = row([]).spacing(2); for (key, label) in choices { options = options.push( @@ -196,7 +196,7 @@ impl App { if argv { self.draft.argv(&path) } else { - self.draft.string(&path, default) + self.wallpaper_field_value(&path, default) } }); let commit = field.clone(); diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index b3e968e..6aadc6a 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -12,6 +12,7 @@ mod fonts; mod schema; mod store; mod visuals; +mod wallpaper_controls; mod watch; use std::collections::HashMap; @@ -28,7 +29,7 @@ use store::{Edit, Snapshot, set}; fn main() -> cosmic::iced::Result { let mut args = std::env::args_os().skip(1); let mut path = store::config_path(); - let mut connection_tab = None; + let mut initial_page = None; while let Some(arg) = args.next() { match arg.to_str() { Some("--config") => { @@ -39,17 +40,18 @@ fn main() -> cosmic::iced::Result { path = PathBuf::from(value); } Some("--page") => { - connection_tab = match args.next().as_deref().and_then(|s| s.to_str()) { - Some("connections" | "wifi") => Some(connections::Tab::Wifi), - Some("bluetooth") => Some(connections::Tab::Bluetooth), + initial_page = match args.next().as_deref().and_then(|s| s.to_str()) { + Some("connections" | "wifi") => Some(InitialPage::Connections(connections::Tab::Wifi)), + Some("bluetooth") => Some(InitialPage::Connections(connections::Tab::Bluetooth)), + Some("wallpaper") => Some(InitialPage::Wallpaper), _ => { - eprintln!("--page accepts connections, wifi, or bluetooth"); + eprintln!("--page accepts connections, wifi, bluetooth, or wallpaper"); return Ok(()); } }; } _ => { - eprintln!("Usage: ferese-settings [--config PATH] [--page connections|wifi|bluetooth]"); + eprintln!("Usage: ferese-settings [--config PATH] [--page connections|wifi|bluetooth|wallpaper]"); return Ok(()); } } @@ -65,10 +67,16 @@ fn main() -> cosmic::iced::Result { .is_daemon(false) .antialiasing(true) .default_text_size(14.), - (path, initial, connection_tab), + (path, initial, initial_page), ) } +#[derive(Clone, Copy)] +enum InitialPage { + Connections(connections::Tab), + Wallpaper, +} + #[derive(Clone, Debug)] enum Message { Connections(connections::Input), @@ -109,12 +117,16 @@ enum Message { ThemeImported(Result, String>), ExpireUndo(u64), PickWallpaper, + WallpaperTarget(wallpaper_controls::Target), + UseDefaultWallpaper(ferese_config::theme::Appearance), + RestoreDefaultWallpapers, + DefaultWallpaperThumbnail(ferese_config::theme::Appearance, Result), PreviewLock, LockPreviewStarted(Result<(), String>), AddNote, NoteAction(String, widget::text_editor::Action), SaveNote(String, u64), - WallpaperPicked(Result, String>), + WallpaperPicked(wallpaper_controls::Target, Result, String>), NewCommand(String), AddCommand, AddSwipe(&'static str), @@ -157,10 +169,7 @@ struct App { auto_details: bool, advanced_theme: bool, theme_file_target: usize, - thumbnail: Option, - thumbnail_path: String, - thumbnail_loading: bool, - thumbnail_error: Option, + wallpaper: wallpaper_controls::State, } fn initial_visible_rows() -> std::collections::HashSet<(&'static str, usize)> { @@ -197,7 +206,7 @@ struct NoteEditor { impl cosmic::Application for App { type Executor = cosmic::executor::Default; - type Flags = (PathBuf, Result, Option); + type Flags = (PathBuf, Result, Option); type Message = Message; const APP_ID: &'static str = "dev.ferese.Settings"; @@ -209,7 +218,7 @@ impl cosmic::Application for App { &mut self.core } - fn init(mut core: Core, (path, initial, connection_tab): Self::Flags) -> (Self, Task) { + fn init(mut core: Core, (path, initial, initial_page): Self::Flags) -> (Self, Task) { core.window.show_headerbar = false; let status = if path == store::config_path() { @@ -226,7 +235,10 @@ impl cosmic::Application for App { let family_sections = theme_controls::FamilySections::new(&resolved.families, ¤t); let mut app = Self { connections: connections::State { - tab: connection_tab.unwrap_or_default(), + tab: match initial_page { + Some(InitialPage::Connections(tab)) => tab, + _ => connections::Tab::default(), + }, ..connections::State::default() }, displays: vec![], @@ -243,10 +255,10 @@ impl cosmic::Application for App { saving_previous: String::new(), error, status, - page: if connection_tab.is_some() { - Page::Connections - } else { - Page::Appearance + page: match initial_page { + Some(InitialPage::Connections(_)) => Page::Connections, + Some(InitialPage::Wallpaper) => Page::Wallpaper, + None => Page::Appearance, }, profile_pages: std::env::var_os("FERESE_PROFILE_SETTINGS").is_some(), page_transition: None, @@ -266,10 +278,7 @@ impl cosmic::Application for App { auto_details: false, advanced_theme: false, theme_file_target: 0, - thumbnail: None, - thumbnail_path: String::new(), - thumbnail_loading: false, - thumbnail_error: None, + wallpaper: Default::default(), }; let task = app .core @@ -283,7 +292,8 @@ impl cosmic::Application for App { } else { Task::none() }; - (app, Task::batch([task, connections])) + let thumbnails = app.load_thumbnail(); + (app, Task::batch([task, connections, thumbnails])) } fn update(&mut self, message: Message) -> Task { @@ -321,6 +331,8 @@ impl cosmic::Application for App { return self.edit_many(displays::edits(&prefix, mode, automatic)); } Message::ThemeChanged(snapshot) => { + let wallpaper_changed = + self.resolved.presented.tokens.background.path != snapshot.presented.tokens.background.path; let font_changed = self.resolved.presented.tokens.typography.font_family != snapshot.presented.tokens.typography.font_family; if self.resolved.families != snapshot.families { @@ -333,7 +345,12 @@ impl cosmic::Application for App { self.font = ferese_theme::font(Some(&self.resolved.presented.tokens.typography.font_family)); } - return self.update_theme(); + let theme = self.update_theme(); + return if wallpaper_changed { + Task::batch([theme, self.load_thumbnail()]) + } else { + theme + }; } Message::NoteAction(id, action) => { if let Some(editor) = self.note_editors.get_mut(&id) { @@ -739,7 +756,8 @@ impl cosmic::Application for App { Message::ThemeImported(Err(error)) => self.error = Some(error), Message::ExpireUndo(revision) if revision == self.undo_revision => self.undo = None, Message::PickWallpaper => { - return cosmic::task::future(async { + let target = self.wallpaper.target; + return cosmic::task::future(async move { let result = tokio::task::spawn_blocking(|| { store::pick_file( "Choose a wallpaper", @@ -748,34 +766,31 @@ impl cosmic::Application for App { }) .await .unwrap_or_else(|e| Err(e.to_string())); - Message::WallpaperPicked(result) + Message::WallpaperPicked(target, result) }); } - Message::WallpaperPicked(Ok(Some(path))) => { - return self.change(set("theme.background.path", path)); + Message::WallpaperPicked(target, Ok(Some(path))) => { + return self.apply_wallpaper_edits(wallpaper_controls::edits(target, "path", path)); } - Message::WallpaperPicked(Err(error)) => self.error = Some(error), + Message::WallpaperPicked(_, Err(error)) => self.error = Some(error), Message::Thumbnail(path, result) => { - self.thumbnail_loading = false; - if path - == self - .current - .string("theme.background.path", ferese_config::default_wallpaper()) - { - self.thumbnail_path = path; - match result { - Ok(handle) => { - self.thumbnail = Some(handle); - self.thumbnail_error = None; - } - Err(error) => { - self.thumbnail = None; - self.thumbnail_error = Some(error); - } - } - } + self.wallpaper_thumbnail(path, result); return self.load_thumbnail(); } + Message::DefaultWallpaperThumbnail(appearance, result) => { + self.default_wallpaper_thumbnail(appearance, result); + } + Message::WallpaperTarget(target) => self.wallpaper.target = target, + Message::UseDefaultWallpaper(appearance) => { + return self.apply_wallpaper_edits(wallpaper_controls::edits( + self.wallpaper.target, + "path", + ferese_config::default_wallpaper_for(appearance), + )); + } + Message::RestoreDefaultWallpapers => { + return self.apply_wallpaper_edits(wallpaper_controls::restore_defaults()); + } Message::NewCommand(value) => self.new_command = value, Message::AddCommand if !self.saving => match shlex::split(&self.new_command).filter(|a| !a.is_empty()) { Some(args) => { @@ -887,57 +902,6 @@ impl App { self.note_editors.retain(|id, _| ids.contains(id)); } - fn load_thumbnail(&mut self) -> Task { - let path = self - .current - .string("theme.background.path", ferese_config::default_wallpaper()); - if self.page != Page::Wallpaper || self.thumbnail_loading || path == self.thumbnail_path { - return Task::none(); - } - if path.is_empty() { - self.thumbnail = None; - self.thumbnail_path.clear(); - self.thumbnail_error = None; - return Task::none(); - } - - self.thumbnail_loading = true; - cosmic::task::future(async move { - let (send, receive) = cosmic::iced::futures::channel::oneshot::channel(); - let file = path.clone(); - std::thread::spawn(move || { - let result = (|| -> Result<_, String> { - let mut reader = image::ImageReader::open(&file) - .map_err(|e| e.to_string())? - .with_guessed_format() - .map_err(|e| e.to_string())?; - let mut limits = image::Limits::default(); - limits.max_alloc = Some(256 * 1024 * 1024); - limits.max_image_width = Some(16384); - limits.max_image_height = Some(16384); - reader.limits(limits); - let pixels = reader - .decode() - .map_err(|e| e.to_string())? - .thumbnail(1000, 500) - .into_rgba8(); - Ok(widget::image::Handle::from_rgba( - pixels.width(), - pixels.height(), - pixels.into_raw(), - )) - })(); - let _ = send.send(result); - }); - Message::Thumbnail( - path, - receive - .await - .unwrap_or_else(|_| Err("Could not load wallpaper preview.".into())), - ) - }) - } - fn edit_many(&mut self, edits: Vec) -> Task { for edit in edits { if let Err(error) = self.draft.edit(&edit) { @@ -960,6 +924,16 @@ impl App { } fn change(&mut self, edit: Edit) -> Task { + if let Edit::Set(path, value) = &edit + && matches!(path.as_str(), "theme.background.path" | "theme.background.mode") + { + return self.apply_wallpaper_edits(wallpaper_controls::edits( + wallpaper_controls::Target::Both, + path.rsplit('.').next().unwrap(), + value.clone(), + )); + } + match self.draft.edit(&edit) { Ok(()) => { // A shared style selection applies to both appearances while @@ -1061,6 +1035,66 @@ mod tests { 1 + tree.children.iter().map(tree_nodes).sum::() } + #[test] + fn wallpaper_preview_uses_presented_state_and_tracks_mode_changes() { + let mut app = app(); + app.path = store::config_path(); + app.current = Snapshot::parse("theme { background { path \"/saved.png\"; }; }".into()).unwrap(); + app.resolved.presented.tokens.background.path = Some("/active.png".into()); + assert_eq!(app.active_wallpaper_path(), "/active.png"); + + app.page = Page::Wallpaper; + let mut snapshot = app.resolved.clone(); + snapshot.presented.tokens.background.path = + Some(ferese_config::default_wallpaper_for(ferese_config::theme::Appearance::Light).into()); + let task = app.update(Message::ThemeChanged(Box::new(snapshot))); + assert!(task.units() > 0); + assert_eq!( + app.active_wallpaper_path(), + ferese_config::default_wallpaper_for(ferese_config::theme::Appearance::Light) + ); + } + + #[test] + fn wallpaper_preview_config_preserves_its_mode_and_relative_paths() { + let mut app = app(); + app.path = PathBuf::from("/preview/config.kdl"); + app.current = Snapshot::parse("theme { mode \"light\"; background { path \"shared.png\"; }; light { background { path \"light.png\"; }; }; }".into()).unwrap(); + assert_eq!(app.active_wallpaper_path(), "/preview/light.png"); + app.wallpaper.target = wallpaper_controls::Target::Dark; + assert_eq!(app.active_wallpaper_path(), "/preview/light.png"); + } + + #[test] + fn choosing_a_default_clears_old_input_and_keeps_the_other_mode() { + let mut app = app(); + app.saving = true; + app.wallpaper.target = wallpaper_controls::Target::Light; + app.draft = Snapshot::parse("theme { dark { background { path \"/custom-dark.png\"; }; }; }".into()).unwrap(); + app.inputs + .insert("theme.light.background.path".into(), "/unfinished.png".into()); + let _ = app.update(Message::UseDefaultWallpaper(ferese_config::theme::Appearance::Light)); + assert!(!app.inputs.contains_key("theme.light.background.path")); + assert_eq!( + app.draft.string("theme.light.background.path", ""), + ferese_config::default_wallpaper_for(ferese_config::theme::Appearance::Light) + ); + assert_eq!(app.draft.string("theme.dark.background.path", ""), "/custom-dark.png"); + } + + #[test] + fn wallpaper_picker_keeps_its_target_when_selection_changes() { + let mut app = app(); + app.saving = true; + app.wallpaper.target = wallpaper_controls::Target::Dark; + let _ = app.update(Message::WallpaperPicked( + wallpaper_controls::Target::Light, + Ok(Some("/chosen.png".into())), + )); + assert_eq!(app.draft.string("theme.light.background.path", ""), "/chosen.png"); + assert!(app.draft.item("theme.dark.background.path").is_none()); + } + #[test] fn shortcuts_build_controls_only_for_mounted_rows() { let mut app = shortcuts(100); diff --git a/shell/ferese-settings/src/pages.rs b/shell/ferese-settings/src/pages.rs index 1e95b5c..621f204 100644 --- a/shell/ferese-settings/src/pages.rs +++ b/shell/ferese-settings/src/pages.rs @@ -179,30 +179,13 @@ impl App { } if self.page == Page::Wallpaper { - if let Some(handle) = &self.thumbnail { - body = body.push( - widget::image(handle.clone()) - .width(Length::Fill) - .height(160) - .content_fit(cosmic::iced::ContentFit::Contain), - ); - } else { - body = body.push(self.note(if self.thumbnail_loading { - "Loading wallpaper preview…" - } else { - self.thumbnail_error - .as_deref() - .unwrap_or("Choose an image to preview your wallpaper.") - })); - } - body = body.push(self.settings_button( - "Choose image…", - "M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01", - Some(Message::PickWallpaper), - false, - )); + body = body.push(self.wallpaper_controls()); } - let fields = schema::fields(self.page); + let fields = if self.page == Page::Wallpaper { + self.wallpaper_fields() + } else { + schema::fields(self.page) + }; if !fields.is_empty() { let mut group = column([]).spacing(1); diff --git a/shell/ferese-settings/src/theme_controls.rs b/shell/ferese-settings/src/theme_controls.rs index 4fa26cf..015aa3f 100644 --- a/shell/ferese-settings/src/theme_controls.rs +++ b/shell/ferese-settings/src/theme_controls.rs @@ -249,8 +249,8 @@ impl App { let list = crate::navigation::gallery_list(appearance, custom); for chunk in 0..self.family_sections.indices(custom).len().div_ceil(3) { let content = widget::responsive(move |size| { - // Three previews: 200:126 image, caption, and tile padding. - let height = ((size.width - 12.) / 3. - 16.).max(0.) * 126. / 200. + 34.; + // Three borderless previews, with names overlaid inside each image. + let height = ((size.width - 12.) / 3.).max(0.) * 126. / 200.; let content: Element<'_, Message> = if self.visible_rows.contains(&(list, chunk)) { self.theme_gallery_row(chunk, appearance, custom) } else { diff --git a/shell/ferese-settings/src/wallpaper_controls.rs b/shell/ferese-settings/src/wallpaper_controls.rs new file mode 100644 index 0000000..1141413 --- /dev/null +++ b/shell/ferese-settings/src/wallpaper_controls.rs @@ -0,0 +1,591 @@ +use cosmic::app::Task; +use cosmic::widget::{button, column, row}; +use ferese_config::theme::Appearance; + +use super::{Alignment, App, Element, Field, Kind, Length, Message, Page, schema, visuals, widget}; +use crate::store::{Edit, Snapshot, set}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(super) enum Target { + #[default] + Both, + Light, + Dark, +} + +impl Target { + const ALL: [Self; 3] = [Self::Both, Self::Light, Self::Dark]; + + fn prefix(self) -> &'static str { + match self { + Self::Both => "theme.background", + Self::Light => "theme.light.background", + Self::Dark => "theme.dark.background", + } + } + + fn appearance(self, active: Appearance) -> Appearance { + match self { + Self::Both => active, + Self::Light => Appearance::Light, + Self::Dark => Appearance::Dark, + } + } +} + +impl std::fmt::Display for Target { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Both => "Both modes", + Self::Light => "Light mode", + Self::Dark => "Dark mode", + }) + } +} + +#[derive(Default)] +pub(super) struct State { + pub target: Target, + active: Preview, + defaults: [Preview; 2], +} + +#[derive(Default)] +struct Preview { + path: String, + requested: Option, + handle: Option, + error: Option, +} + +impl Preview { + fn request(&mut self, path: &str) -> bool { + if self.requested.is_some() || self.path == path { + return false; + } + + self.handle = None; + self.error = None; + if path.is_empty() { + self.path.clear(); + return false; + } + + self.path.clear(); + self.requested = Some(path.to_owned()); + true + } + + fn finish(&mut self, path: String, desired: &str, result: Result) { + if self.requested.as_deref() != Some(&path) { + return; + } + + self.requested = None; + if path != desired { + return; + } + + self.path = path; + match result { + Ok(handle) => { + self.handle = Some(handle); + self.error = None; + } + Err(error) => { + self.handle = None; + self.error = Some(error); + } + } + } +} + +fn index(appearance: Appearance) -> usize { + usize::from(appearance == Appearance::Dark) +} + +pub(super) fn edits(target: Target, token: &str, value: impl Into) -> Vec { + let mut edits = vec![set(&format!("{}.{token}", target.prefix()), value)]; + if target == Target::Both { + for kind in ["light", "dark"] { + edits.push(Edit::Unset(format!("theme.{kind}.background.{token}"))); + } + } + + edits +} + +pub(super) fn restore_defaults() -> Vec { + [Target::Both, Target::Light, Target::Dark] + .map(|target| Edit::Unset(format!("{}.path", target.prefix()))) + .into() +} + +fn setting(snapshot: &Snapshot, target: Target, token: &str, fallback: &str) -> String { + snapshot + .item(&format!("{}.{token}", target.prefix())) + .or_else(|| snapshot.item(&format!("theme.background.{token}"))) + .and_then(serde_json::Value::as_str) + .unwrap_or(fallback) + .to_owned() +} + +fn image_button_style(palette: visuals::Palette, selected: bool) -> cosmic::theme::Button { + let paint = move |hovered: bool, focused: bool| button::Style { + shape: Some(cosmic::iced::border::Shape::Continuous), + background: None, + border_radius: palette.radius.min(10.).into(), + border_width: if selected || focused { + 2. + } else if hovered { + 1. + } else { + 0. + }, + border_color: palette.accent, + ..Default::default() + }; + cosmic::theme::Button::Custom { + active: Box::new(move |focused, _| paint(false, focused)), + hovered: Box::new(move |focused, _| paint(true, focused)), + pressed: Box::new(move |focused, _| paint(true, focused)), + disabled: Box::new(move |_| paint(false, false)), + } +} + +fn thumbnail(path: String, builtin: Option) -> Task { + cosmic::task::future(async move { + let (send, receive) = cosmic::iced::futures::channel::oneshot::channel(); + let file = path.clone(); + std::thread::spawn(move || { + let result = (|| -> Result<_, String> { + let mut reader = image::ImageReader::open(&file) + .map_err(|e| e.to_string())? + .with_guessed_format() + .map_err(|e| e.to_string())?; + let mut limits = image::Limits::default(); + limits.max_alloc = Some(256 * 1024 * 1024); + limits.max_image_width = Some(16384); + limits.max_image_height = Some(16384); + reader.limits(limits); + let (width, height) = if builtin.is_some() { (400, 225) } else { (1000, 500) }; + let pixels = reader + .decode() + .map_err(|e| e.to_string())? + .thumbnail(width, height) + .into_rgba8(); + Ok(widget::image::Handle::from_rgba( + pixels.width(), + pixels.height(), + pixels.into_raw(), + )) + })(); + let _ = send.send(result); + }); + let result = receive + .await + .unwrap_or_else(|_| Err("Could not load wallpaper preview.".into())); + match builtin { + Some(appearance) => Message::DefaultWallpaperThumbnail(appearance, result), + None => Message::Thumbnail(path, result), + } + }) +} + +impl App { + pub(super) fn apply_wallpaper_edits(&mut self, edits: Vec) -> Task { + for edit in &edits { + if let Edit::Set(path, _) | Edit::Unset(path) = edit { + self.inputs.remove(path); + } + } + + self.edit_many(edits) + } + + pub(super) fn active_wallpaper_path(&self) -> String { + if self.path == crate::store::config_path() { + return self + .resolved + .presented + .tokens + .background + .path + .as_ref() + .map(|path| path.to_string_lossy().into_owned()) + .unwrap_or_default(); + } + + // A separate config preview must not borrow the live desktop's path. + let appearance = match self.current.string("theme.mode", "auto").as_str() { + "light" => Appearance::Light, + "dark" => Appearance::Dark, + _ => self.resolved.theme.appearance, + }; + let target = if appearance == Appearance::Light { + Target::Light + } else { + Target::Dark + }; + let path = setting( + &self.current, + target, + "path", + ferese_config::default_wallpaper_for(appearance), + ); + if path.is_empty() { + return path; + } + + ferese_config::theme::theme_path(self.path.parent().unwrap_or(std::path::Path::new(".")), path.as_ref()) + .to_string_lossy() + .into_owned() + } + + pub(super) fn load_thumbnail(&mut self) -> Task { + if self.page != Page::Wallpaper { + return Task::none(); + } + + let mut tasks = Vec::with_capacity(3); + let path = self.active_wallpaper_path(); + if self.wallpaper.active.request(&path) { + tasks.push(thumbnail(path, None)); + } + + for appearance in [Appearance::Light, Appearance::Dark] { + let path = ferese_config::default_wallpaper_for(appearance); + if self.wallpaper.defaults[index(appearance)].request(path) { + tasks.push(thumbnail(path.into(), Some(appearance))); + } + } + + Task::batch(tasks) + } + + pub(super) fn wallpaper_thumbnail(&mut self, path: String, result: Result) { + let desired = self.active_wallpaper_path(); + self.wallpaper.active.finish(path, &desired, result); + } + + pub(super) fn default_wallpaper_thumbnail( + &mut self, + appearance: Appearance, + result: Result, + ) { + let path = ferese_config::default_wallpaper_for(appearance); + self.wallpaper.defaults[index(appearance)].finish(path.into(), path, result); + } + + pub(super) fn wallpaper_fields(&self) -> Vec { + let active = if self.path == crate::store::config_path() { + self.resolved.presented.appearance + } else { + match self.current.string("theme.mode", "auto").as_str() { + "light" => Appearance::Light, + "dark" => Appearance::Dark, + _ => self.resolved.theme.appearance, + } + }; + schema::fields(Page::Wallpaper) + .into_iter() + .map(|mut field| { + field.path = field.path.replace("theme.background", self.wallpaper.target.prefix()); + if let Kind::Text { default, .. } = &mut field.kind { + *default = ferese_config::default_wallpaper_for(self.wallpaper.target.appearance(active)); + } + + field + }) + .collect() + } + + pub(super) fn wallpaper_field_value(&self, path: &str, fallback: &str) -> String { + if path.starts_with(self.wallpaper.target.prefix()) { + return setting( + &self.draft, + self.wallpaper.target, + path.rsplit('.').next().unwrap(), + fallback, + ); + } + + self.draft.string(path, fallback) + } + + pub(super) fn wallpaper_controls(&self) -> Element<'_, Message> { + let palette = visuals::Palette::from_resolved(&self.resolved.presented); + let preview = &self.wallpaper.active; + let mut body = column([]).spacing(8).push(self.label("Current wallpaper", 13.)); + if let Some(handle) = &preview.handle { + body = body.push( + widget::container( + widget::image(handle.clone()) + .border_radius(palette.radius.min(10.)) + .shape(cosmic::iced::border::Shape::Continuous) + .width(Length::Shrink) + .height(180) + .content_fit(cosmic::iced::ContentFit::Contain), + ) + .center_x(Length::Fill), + ); + } else { + body = body.push(self.note(if preview.requested.is_some() { + "Loading wallpaper preview…" + } else { + preview.error.as_deref().unwrap_or("No wallpaper image selected.") + })); + } + + body = body.push( + row([]) + .spacing(8) + .align_y(Alignment::Center) + .push(self.label("Apply to", 12.)) + .push(ferese_theme::controls::select( + cosmic::iced::widget::pick_list(Target::ALL, Some(self.wallpaper.target), Message::WallpaperTarget) + .font(self.font) + .text_size(12), + palette, + )) + .push(widget::Space::new().width(Length::Fill)) + .push(self.settings_button( + "Choose image…", + "M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01", + Some(Message::PickWallpaper), + false, + )), + ); + let mut defaults = row([]).spacing(8); + let active = self.active_wallpaper_path(); + for appearance in [Appearance::Light, Appearance::Dark] { + let name = if appearance == Appearance::Light { + "Light" + } else { + "Dark" + }; + let preview = &self.wallpaper.defaults[index(appearance)]; + let image: Element<'_, Message> = if let Some(handle) = &preview.handle { + widget::image(handle.clone()) + .border_radius(palette.radius.min(10.)) + .shape(cosmic::iced::border::Shape::Continuous) + .width(Length::Fill) + .height(110) + .content_fit(cosmic::iced::ContentFit::Cover) + .into() + } else { + widget::container(self.note(preview.error.as_deref().unwrap_or("Loading…"))) + .width(Length::Fill) + .height(110) + .into() + }; + let label_color = if preview.handle.is_none() { + palette.text + } else if appearance == Appearance::Light { + cosmic::iced::Color::from_rgb8(24, 33, 44) + } else { + cosmic::iced::Color::WHITE + }; + let label = widget::container(self.label(name, 12.).class(cosmic::theme::Text::Color(label_color))) + .padding(8) + .width(Length::Fill) + .height(Length::Fill); + let tile = cosmic::iced::widget::stack([image, label.into()]); + + defaults = defaults.push( + button::custom(tile) + .name(format!("Use default {name} wallpaper")) + .padding(0) + .width(Length::Fill) + .class(image_button_style( + palette, + active == ferese_config::default_wallpaper_for(appearance), + )) + .on_press(Message::UseDefaultWallpaper(appearance)), + ); + } + + body.push( + row([]) + .spacing(8) + .align_y(Alignment::Center) + .push(self.label("Default wallpapers", 13.)) + .push(widget::Space::new().width(Length::Fill)) + .push(self.settings_button( + "Match appearance", + "M20 7v5h-5 M4 17v-5h5 M6 7a7 7 0 0 1 12-2l2 2 M18 17a7 7 0 0 1-12 2l-2-2", + Some(Message::RestoreDefaultWallpapers), + false, + )), + ) + .push(defaults) + .into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore = "release UI preview; requires headless renderer backends"] + fn render_preview_pages_on_both_backends() { + use cosmic::Application; + use cosmic::iced::advanced::renderer::{Headless, Renderer as _}; + use cosmic::iced::advanced::{Layout, layout, mouse, renderer, widget::Tree}; + use cosmic::iced::{Color, Font, Pixels, Rectangle, Size}; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut app = App::init( + cosmic::app::Core::default(), + ("/preview/config.kdl".into(), Snapshot::parse(String::new()), None), + ) + .0; + for appearance in [Appearance::Light, Appearance::Dark] { + let path = ferese_config::default_wallpaper_for(appearance); + let pixels = image::open(path).unwrap().thumbnail(400, 225).into_rgba8(); + let handle = widget::image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw()); + assert!(app.wallpaper.defaults[index(appearance)].request(path)); + app.default_wallpaper_thumbnail(appearance, Ok(handle)); + } + + for backend in ["wgpu", "tiny-skia"] { + let mut renderer = runtime + .block_on(::new( + Font::default(), + Pixels(14.), + Some(backend), + )) + .expect("requested renderer"); + for appearance in [Appearance::Light, Appearance::Dark] { + let name = if appearance == Appearance::Light { + "light" + } else { + "dark" + }; + app.current = Snapshot::parse(format!("theme {{ mode \"{name}\"; }}")).unwrap(); + app.draft = app.current.clone(); + app.visible_rows = (0..app.resolved.families.len().div_ceil(3)) + .map(|row| ("theme", row)) + .collect(); + let pixels = image::open(ferese_config::default_wallpaper_for(appearance)) + .unwrap() + .thumbnail(1000, 500) + .into_rgba8(); + app.wallpaper.active.handle = Some(widget::image::Handle::from_rgba( + pixels.width(), + pixels.height(), + pixels.into_raw(), + )); + for page in [Page::Wallpaper, Page::Appearance] { + app.page = page; + let bounds = Rectangle::with_size(Size::new(1000., 780.)); + let theme = app.native_palette.native_theme(); + let mut view = app.page_view(); + let mut tree = Tree::new(view.as_widget()); + let node = view.as_widget_mut().layout( + &mut tree, + &renderer, + &layout::Limits::new(Size::ZERO, bounds.size()), + ); + renderer.reset(bounds); + view.as_widget().draw( + &tree, + &mut renderer, + &theme, + &renderer::Style { + text_color: Color::WHITE, + icon_color: Color::WHITE, + scale_factor: 1.25, + }, + Layout::new(&node), + mouse::Cursor::Unavailable, + &bounds, + ); + let pixels = Headless::screenshot( + &mut renderer, + Size::new(1250, 975), + 1.25, + theme.cosmic().bg_color().into(), + ); + let preview = if page == Page::Wallpaper { "wallpaper" } else { "theme" }; + let path = format!("/tmp/ferese-{preview}-{backend}-{name}.png"); + image::save_buffer(&path, &pixels, 1250, 975, image::ColorType::Rgba8).unwrap(); + eprintln!("Preview page: {path}"); + } + } + } + } + + #[test] + fn selecting_both_removes_mode_overrides_without_changing_lock_wallpaper() { + let mut snapshot = Snapshot::parse( + "theme { background { lock-path \"/lock.png\"; }; light { background { path \"/light.png\"; }; }; dark { background { path \"/dark.png\"; }; }; }".into(), + ).unwrap(); + for edit in edits(Target::Both, "path", "/shared.png") { + snapshot.edit(&edit).unwrap(); + } + + assert_eq!(snapshot.string("theme.background.path", ""), "/shared.png"); + assert!(snapshot.item("theme.light.background.path").is_none()); + assert!(snapshot.item("theme.dark.background.path").is_none()); + assert_eq!(snapshot.string("theme.background.lock_path", ""), "/lock.png"); + } + + #[test] + fn selecting_one_mode_preserves_the_other_and_inherits_shared_settings() { + let mut snapshot = + Snapshot::parse("theme { background { path \"/shared.png\"; mode \"fit\"; }; }".into()).unwrap(); + for edit in edits(Target::Light, "path", "/light.png") { + snapshot.edit(&edit).unwrap(); + } + + assert_eq!(setting(&snapshot, Target::Light, "path", ""), "/light.png"); + assert_eq!(setting(&snapshot, Target::Dark, "path", ""), "/shared.png"); + assert_eq!(setting(&snapshot, Target::Light, "mode", "fill"), "fit"); + } + + #[test] + fn restoring_defaults_keeps_placement_and_lock_image() { + let mut snapshot = Snapshot::parse("theme { background { path \"/custom.png\"; lock-path \"/lock.png\"; mode \"fit\"; }; light { background { path \"/light.png\"; }; }; }".into()).unwrap(); + for edit in restore_defaults() { + snapshot.edit(&edit).unwrap(); + } + + for target in Target::ALL { + assert!(snapshot.item(&format!("{}.path", target.prefix())).is_none()); + } + + assert_eq!(snapshot.string("theme.background.mode", ""), "fit"); + assert_eq!(snapshot.string("theme.background.lock_path", ""), "/lock.png"); + } + + #[test] + fn returning_to_a_previous_path_during_a_load_restarts_its_preview() { + let mut preview = Preview::default(); + let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]); + assert!(preview.request("/first.png")); + preview.finish("/first.png".into(), "/first.png", Ok(image.clone())); + assert!(preview.request("/second.png")); + preview.finish("/second.png".into(), "/first.png", Ok(image)); + assert!(preview.request("/first.png")); + } + + #[test] + fn stale_preview_completion_does_not_replace_the_active_wallpaper() { + let mut preview = Preview::default(); + assert!(preview.request("/old.png")); + let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]); + preview.finish("/old.png".into(), "/new.png", Ok(image.clone())); + assert!(preview.handle.is_none()); + assert!(preview.request("/new.png")); + preview.finish("/old.png".into(), "/new.png", Ok(image.clone())); + assert_eq!(preview.requested.as_deref(), Some("/new.png")); + preview.finish("/new.png".into(), "/new.png", Ok(image)); + assert!(preview.handle.is_some()); + assert!(!preview.request("/new.png")); + } +} diff --git a/site/README.md b/site/README.md index 0fac9b9..dca3df8 100644 --- a/site/README.md +++ b/site/README.md @@ -35,3 +35,20 @@ The site uses relative links, including for handbook search, so it also works un For deployment setup, follow [GitHub’s custom workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages). + +## Brand assets + +The Ferese symbols live in `assets/branding`. The website uses +`ferese-blue-dark.svg`. The README chooses the light or dark SVG to match +the reader's appearance and uses the flat blue SVG as its fallback. Keep their +1024×1024 canvas square so the symbol and glow are not stretched or cropped. + +The favicon uses the flat blue symbol. `site/assets/favicon-32.png` is 32×32; +`site/assets/favicon.ico` contains 16×16, 32×32 and 48×48 images. These files are +checked in, so building the site needs no icon renderer. Their links work at +both the site root and the GitHub Pages project path. + +The light and dark wallpapers live in `assets/wallpapers`. Both are +3840×2160 and are bundled by the installer. Ferese selects the matching image +when no custom wallpaper is set. Settings keeps both defaults available below +the current wallpaper preview. Existing screenshots show the previous artwork. diff --git a/site/assets/favicon-32.png b/site/assets/favicon-32.png new file mode 100644 index 0000000..3cbf4fe Binary files /dev/null and b/site/assets/favicon-32.png differ diff --git a/site/assets/favicon.ico b/site/assets/favicon.ico new file mode 100644 index 0000000..815b074 Binary files /dev/null and b/site/assets/favicon.ico differ diff --git a/site/assets/fonts/Figtree-OFL.txt b/site/assets/fonts/Figtree-OFL.txt new file mode 100644 index 0000000..80b12bb --- /dev/null +++ b/site/assets/fonts/Figtree-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/assets/fonts/Figtree-Variable.ttf b/site/assets/fonts/Figtree-Variable.ttf new file mode 100644 index 0000000..579e2ab Binary files /dev/null and b/site/assets/fonts/Figtree-Variable.ttf differ diff --git a/site/index.html b/site/index.html index a386934..fb58ae2 100644 --- a/site/index.html +++ b/site/index.html @@ -12,17 +12,20 @@ Ferese — Wayland desktop - - + + + +