Customizable "Open with" applications for repository clone (#29320)
Users could customize the "clone" menu with their own application URLs on the admin panel. Replace #22378 Close #21121 Close #22149
|
@ -15,8 +15,45 @@ type PictureStruct struct {
|
||||||
EnableFederatedAvatar *config.Value[bool]
|
EnableFederatedAvatar *config.Value[bool]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenWithEditorApp struct {
|
||||||
|
DisplayName string
|
||||||
|
OpenURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWithEditorAppsType []OpenWithEditorApp
|
||||||
|
|
||||||
|
func (t OpenWithEditorAppsType) ToTextareaString() string {
|
||||||
|
ret := ""
|
||||||
|
for _, app := range t {
|
||||||
|
ret += app.DisplayName + " = " + app.OpenURL + "\n"
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
|
||||||
|
return OpenWithEditorAppsType{
|
||||||
|
{
|
||||||
|
DisplayName: "VS Code",
|
||||||
|
OpenURL: "vscode://vscode.git/clone?url={url}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "VSCodium",
|
||||||
|
OpenURL: "vscodium://vscode.git/clone?url={url}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "Intellij IDEA",
|
||||||
|
OpenURL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepositoryStruct struct {
|
||||||
|
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigStruct struct {
|
type ConfigStruct struct {
|
||||||
Picture *PictureStruct
|
Picture *PictureStruct
|
||||||
|
Repository *RepositoryStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -28,8 +65,11 @@ func initDefaultConfig() {
|
||||||
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
|
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
|
||||||
defaultConfig = &ConfigStruct{
|
defaultConfig = &ConfigStruct{
|
||||||
Picture: &PictureStruct{
|
Picture: &PictureStruct{
|
||||||
DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
|
DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
|
||||||
EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
|
EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
|
||||||
|
},
|
||||||
|
Repository: &RepositoryStruct{
|
||||||
|
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +82,9 @@ func Config() *ConfigStruct {
|
||||||
type cfgSecKeyGetter struct{}
|
type cfgSecKeyGetter struct{}
|
||||||
|
|
||||||
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
|
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
|
||||||
|
if key == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
cfgSec, err := CfgProvider.GetSection(sec)
|
cfgSec, err := CfgProvider.GetSection(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get config section: %q", sec)
|
log.Error("Unable to get config section: %q", sec)
|
||||||
|
|
|
@ -5,8 +5,11 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CfgSecKey struct {
|
type CfgSecKey struct {
|
||||||
|
@ -23,14 +26,14 @@ type Value[T any] struct {
|
||||||
revision int
|
revision int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (value *Value[T]) parse(s string) (v T) {
|
func (value *Value[T]) parse(key, valStr string) (v T) {
|
||||||
switch any(v).(type) {
|
v = value.def
|
||||||
case bool:
|
if valStr != "" {
|
||||||
b, _ := strconv.ParseBool(s)
|
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
|
||||||
return any(b).(T)
|
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
|
||||||
default:
|
}
|
||||||
panic("unsupported config type, please complete the code")
|
|
||||||
}
|
}
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (value *Value[T]) Value(ctx context.Context) (v T) {
|
func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||||
|
@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||||
if valStr == nil {
|
if valStr == nil {
|
||||||
v = value.def
|
v = value.def
|
||||||
} else {
|
} else {
|
||||||
v = value.parse(*valStr)
|
v = value.parse(value.dynKey, *valStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
value.mu.Lock()
|
value.mu.Lock()
|
||||||
|
@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
|
||||||
return value.dynKey
|
return value.dynKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
|
func (value *Value[T]) WithDefault(def T) *Value[T] {
|
||||||
return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
|
value.def = def
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
|
||||||
|
value.cfgSecKey = cfgSecKey
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValueJSON[T any](dynKey string) *Value[T] {
|
||||||
|
return &Value[T]{dynKey: dynKey}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1015,8 +1015,7 @@ fork_branch = Branch to be cloned to the fork
|
||||||
all_branches = All branches
|
all_branches = All branches
|
||||||
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
||||||
use_template = Use this template
|
use_template = Use this template
|
||||||
clone_in_vsc = Clone in VS Code
|
open_with_editor = Open with %s
|
||||||
clone_in_vscodium = Clone in VSCodium
|
|
||||||
download_zip = Download ZIP
|
download_zip = Download ZIP
|
||||||
download_tar = Download TAR.GZ
|
download_tar = Download TAR.GZ
|
||||||
download_bundle = Download BUNDLE
|
download_bundle = Download BUNDLE
|
||||||
|
@ -2833,6 +2832,8 @@ authentication = Authentication sources
|
||||||
emails = User emails
|
emails = User emails
|
||||||
config = Configuration
|
config = Configuration
|
||||||
notices = System notices
|
notices = System notices
|
||||||
|
config_summary = Summary
|
||||||
|
config_settings = Settings
|
||||||
monitor = Monitoring
|
monitor = Monitoring
|
||||||
first_page = First
|
first_page = First
|
||||||
last_page = Last
|
last_page = Last
|
||||||
|
@ -3271,6 +3272,7 @@ config.picture_config = Picture and avatar configuration
|
||||||
config.picture_service = Picture service
|
config.picture_service = Picture service
|
||||||
config.disable_gravatar = Disable Gravatar
|
config.disable_gravatar = Disable Gravatar
|
||||||
config.enable_federated_avatar = Enable federated avatars
|
config.enable_federated_avatar = Enable federated avatars
|
||||||
|
config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
|
||||||
|
|
||||||
config.git_config = Git configuration
|
config.git_config = Git configuration
|
||||||
config.git_disable_diff_highlight = Disable diff syntax highlighting
|
config.git_disable_diff_highlight = Disable diff syntax highlighting
|
||||||
|
|
1
public/assets/img/svg/gitea-open-with-jetbrains.svg
generated
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-open-with-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-open-with-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-open-with-jetbrains__a)"/><linearGradient id="gitea-open-with-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__b)"/><linearGradient id="gitea-open-with-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__c)"/><linearGradient id="gitea-open-with-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-open-with-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 3 KiB |
1
public/assets/img/svg/gitea-open-with-vscode.svg
generated
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-open-with-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
|
After Width: | Height: | Size: 406 B |
1
public/assets/img/svg/gitea-open-with-vscodium.svg
generated
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-open-with-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
public/assets/img/svg/gitea-vscode.svg
generated
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
|
|
Before Width: | Height: | Size: 396 B |
|
@ -7,11 +7,11 @@ package admin
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
system_model "code.gitea.io/gitea/models/system"
|
system_model "code.gitea.io/gitea/models/system"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -24,7 +24,10 @@ import (
|
||||||
"gitea.com/go-chi/session"
|
"gitea.com/go-chi/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tplConfig base.TplName = "admin/config"
|
const (
|
||||||
|
tplConfig base.TplName = "admin/config"
|
||||||
|
tplConfigSettings base.TplName = "admin/config_settings"
|
||||||
|
)
|
||||||
|
|
||||||
// SendTestMail send test mail to confirm mail service is OK
|
// SendTestMail send test mail to confirm mail service is OK
|
||||||
func SendTestMail(ctx *context.Context) {
|
func SendTestMail(ctx *context.Context) {
|
||||||
|
@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
|
||||||
|
|
||||||
// Config show admin config page
|
// Config show admin config page
|
||||||
func Config(ctx *context.Context) {
|
func Config(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("admin.config")
|
ctx.Data["Title"] = ctx.Tr("admin.config_summary")
|
||||||
ctx.Data["PageIsAdminConfig"] = true
|
ctx.Data["PageIsAdminConfig"] = true
|
||||||
|
ctx.Data["PageIsAdminConfigSummary"] = true
|
||||||
|
|
||||||
ctx.Data["CustomConf"] = setting.CustomConf
|
ctx.Data["CustomConf"] = setting.CustomConf
|
||||||
ctx.Data["AppUrl"] = setting.AppURL
|
ctx.Data["AppUrl"] = setting.AppURL
|
||||||
|
@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
|
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
|
||||||
config.GetDynGetter().InvalidateCache()
|
config.GetDynGetter().InvalidateCache()
|
||||||
ctx.Data["SystemConfig"] = setting.Config()
|
|
||||||
prepareDeprecatedWarningsAlert(ctx)
|
prepareDeprecatedWarningsAlert(ctx)
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplConfig)
|
ctx.HTML(http.StatusOK, tplConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConfigSettings(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.config_settings")
|
||||||
|
ctx.Data["PageIsAdminConfig"] = true
|
||||||
|
ctx.Data["PageIsAdminConfigSettings"] = true
|
||||||
|
ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
|
||||||
|
ctx.HTML(http.StatusOK, tplConfigSettings)
|
||||||
|
}
|
||||||
|
|
||||||
func ChangeConfig(ctx *context.Context) {
|
func ChangeConfig(ctx *context.Context) {
|
||||||
key := strings.TrimSpace(ctx.FormString("key"))
|
key := strings.TrimSpace(ctx.FormString("key"))
|
||||||
value := ctx.FormString("value")
|
value := ctx.FormString("value")
|
||||||
cfg := setting.Config()
|
cfg := setting.Config()
|
||||||
allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
|
|
||||||
if !allowedKeys.Contains(key) {
|
marshalBool := func(v string) (string, error) {
|
||||||
|
if b, _ := strconv.ParseBool(v); b {
|
||||||
|
return "true", nil
|
||||||
|
}
|
||||||
|
return "false", nil
|
||||||
|
}
|
||||||
|
marshalOpenWithApps := func(value string) (string, error) {
|
||||||
|
lines := strings.Split(value, "\n")
|
||||||
|
var openWithEditorApps setting.OpenWithEditorAppsType
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
displayName, openURL, ok := strings.Cut(line, "=")
|
||||||
|
displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
|
||||||
|
if !ok || displayName == "" || openURL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
|
||||||
|
DisplayName: strings.TrimSpace(displayName),
|
||||||
|
OpenURL: strings.TrimSpace(openURL),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(openWithEditorApps)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
marshallers := map[string]func(string) (string, error){
|
||||||
|
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
|
||||||
|
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||||
|
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||||
|
}
|
||||||
|
marshaller, hasMarshaller := marshallers[key]
|
||||||
|
if !hasMarshaller {
|
||||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
|
marshaledValue, err := marshaller(value)
|
||||||
log.Error("set setting failed: %v", err)
|
if err != nil {
|
||||||
|
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
|
||||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import (
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/svg"
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/web/feed"
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
@ -812,7 +813,7 @@ func Home(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCode(ctx)
|
renderHomeCode(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
|
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
|
||||||
|
@ -932,9 +933,33 @@ func renderRepoTopics(ctx *context.Context) {
|
||||||
ctx.Data["Topics"] = topics
|
ctx.Data["Topics"] = topics
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderCode(ctx *context.Context) {
|
func prepareOpenWithEditorApps(ctx *context.Context) {
|
||||||
|
var tmplApps []map[string]any
|
||||||
|
apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
|
||||||
|
if len(apps) == 0 {
|
||||||
|
apps = setting.DefaultOpenWithEditorApps()
|
||||||
|
}
|
||||||
|
for _, app := range apps {
|
||||||
|
schema, _, _ := strings.Cut(app.OpenURL, ":")
|
||||||
|
var iconHTML template.HTML
|
||||||
|
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
|
||||||
|
iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-open-with-%s", schema), 16, "gt-mr-3")
|
||||||
|
} else {
|
||||||
|
iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
|
||||||
|
}
|
||||||
|
tmplApps = append(tmplApps, map[string]any{
|
||||||
|
"DisplayName": app.DisplayName,
|
||||||
|
"OpenURL": app.OpenURL,
|
||||||
|
"IconHTML": iconHTML,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx.Data["OpenWithEditorApps"] = tmplApps
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderHomeCode(ctx *context.Context) {
|
||||||
ctx.Data["PageIsViewCode"] = true
|
ctx.Data["PageIsViewCode"] = true
|
||||||
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
|
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
|
||||||
|
prepareOpenWithEditorApps(ctx)
|
||||||
|
|
||||||
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
||||||
showEmpty := true
|
showEmpty := true
|
||||||
|
|
|
@ -691,6 +691,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("", admin.Config)
|
m.Get("", admin.Config)
|
||||||
m.Post("", admin.ChangeConfig)
|
m.Post("", admin.ChangeConfig)
|
||||||
m.Post("/test_mail", admin.SendTestMail)
|
m.Post("/test_mail", admin.SendTestMail)
|
||||||
|
m.Get("/settings", admin.ConfigSettings)
|
||||||
})
|
})
|
||||||
|
|
||||||
m.Group("/monitor", func() {
|
m.Group("/monitor", func() {
|
||||||
|
|
|
@ -192,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
||||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||||
|
|
||||||
|
ctx.Data["SystemConfig"] = setting.Config()
|
||||||
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
|
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
|
||||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
|
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
|
||||||
|
|
||||||
|
|
|
@ -285,27 +285,6 @@
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="ui top attached header">
|
|
||||||
{{ctx.Locale.Tr "admin.config.picture_config"}}
|
|
||||||
</h4>
|
|
||||||
<div class="ui attached table segment">
|
|
||||||
<dl class="admin-dl-horizontal">
|
|
||||||
<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
|
|
||||||
<dd>
|
|
||||||
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
|
|
||||||
<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
|
|
||||||
<dd>
|
|
||||||
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
|
|
||||||
<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="ui top attached header">
|
<h4 class="ui top attached header">
|
||||||
{{ctx.Locale.Tr "admin.config.git_config"}}
|
{{ctx.Locale.Tr "admin.config.git_config"}}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
42
templates/admin/config_settings.tmpl
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "admin.config.picture_config"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached table segment">
|
||||||
|
<dl class="admin-dl-horizontal">
|
||||||
|
<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
|
||||||
|
<dd>
|
||||||
|
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
|
||||||
|
<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
|
||||||
|
<dd>
|
||||||
|
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
|
||||||
|
<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "repository"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/admin/config?key={{.SystemConfig.Repository.OpenWithEditorApps.DynKey}}">
|
||||||
|
<div class="field">
|
||||||
|
<details>
|
||||||
|
<summary>{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}}</summary>
|
||||||
|
<pre class="gt-px-4">{{.DefaultOpenWithEditorAppsString}}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="value">{{(.SystemConfig.Repository.OpenWithEditorApps.Value ctx).ToTextareaString}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{template "admin/layout_footer" .}}
|
|
@ -77,9 +77,17 @@
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a class="{{if .PageIsAdminConfig}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
|
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
|
||||||
{{ctx.Locale.Tr "admin.config"}}
|
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
|
||||||
</a>
|
<div class="menu">
|
||||||
|
<a class="{{if .PageIsAdminConfigSummary}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
|
||||||
|
{{ctx.Locale.Tr "admin.config_summary"}}
|
||||||
|
</a>
|
||||||
|
<a class="{{if .PageIsAdminConfigSettings}}active {{end}}item" href="{{AppSubUrl}}/admin/config/settings">
|
||||||
|
{{ctx.Locale.Tr "admin.config_settings"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/admin/notices">
|
<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/admin/notices">
|
||||||
{{ctx.Locale.Tr "admin.notices"}}
|
{{ctx.Locale.Tr "admin.notices"}}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -43,8 +43,8 @@
|
||||||
for (const el of document.getElementsByClassName('js-clone-url')) {
|
for (const el of document.getElementsByClassName('js-clone-url')) {
|
||||||
el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
|
el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
|
||||||
}
|
}
|
||||||
for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
|
for (const el of document.getElementsByClassName('js-clone-url-editor')) {
|
||||||
el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link);
|
el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -145,7 +145,9 @@
|
||||||
<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
|
<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a>
|
{{range .OpenWithEditorApps}}
|
||||||
|
<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
||||||
|
|
62
web_src/svg/gitea-open-with-jetbrains.svg
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893">
|
||||||
|
<stop offset="0.2581" style="stop-color:#F97A12"/>
|
||||||
|
<stop offset="0.4591" style="stop-color:#B07B58"/>
|
||||||
|
<stop offset="0.7241" style="stop-color:#577BAE"/>
|
||||||
|
<stop offset="0.9105" style="stop-color:#1E7CE5"/>
|
||||||
|
<stop offset="1" style="stop-color:#087CFA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 "/>
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57">
|
||||||
|
<stop offset="0" style="stop-color:#F97A12"/>
|
||||||
|
<stop offset="7.179946e-002" style="stop-color:#CB7A3E"/>
|
||||||
|
<stop offset="0.1541" style="stop-color:#9E7B6A"/>
|
||||||
|
<stop offset="0.242" style="stop-color:#757B91"/>
|
||||||
|
<stop offset="0.3344" style="stop-color:#537BB1"/>
|
||||||
|
<stop offset="0.4324" style="stop-color:#387CCC"/>
|
||||||
|
<stop offset="0.5381" style="stop-color:#237CE0"/>
|
||||||
|
<stop offset="0.6552" style="stop-color:#147CEF"/>
|
||||||
|
<stop offset="0.7925" style="stop-color:#0B7CF7"/>
|
||||||
|
<stop offset="1" style="stop-color:#087CFA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 "/>
|
||||||
|
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191">
|
||||||
|
<stop offset="0" style="stop-color:#FE315D"/>
|
||||||
|
<stop offset="7.840246e-002" style="stop-color:#CB417E"/>
|
||||||
|
<stop offset="0.1601" style="stop-color:#9E4E9B"/>
|
||||||
|
<stop offset="0.2474" style="stop-color:#755BB4"/>
|
||||||
|
<stop offset="0.3392" style="stop-color:#5365CA"/>
|
||||||
|
<stop offset="0.4365" style="stop-color:#386DDB"/>
|
||||||
|
<stop offset="0.5414" style="stop-color:#2374E9"/>
|
||||||
|
<stop offset="0.6576" style="stop-color:#1478F3"/>
|
||||||
|
<stop offset="0.794" style="stop-color:#0B7BF8"/>
|
||||||
|
<stop offset="1" style="stop-color:#087CFA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 "/>
|
||||||
|
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58">
|
||||||
|
<stop offset="0" style="stop-color:#FE315D"/>
|
||||||
|
<stop offset="4.023279e-002" style="stop-color:#F63462"/>
|
||||||
|
<stop offset="0.1037" style="stop-color:#DF3A71"/>
|
||||||
|
<stop offset="0.1667" style="stop-color:#C24383"/>
|
||||||
|
<stop offset="0.2912" style="stop-color:#AD4A91"/>
|
||||||
|
<stop offset="0.5498" style="stop-color:#755BB4"/>
|
||||||
|
<stop offset="0.9175" style="stop-color:#1D76ED"/>
|
||||||
|
<stop offset="1" style="stop-color:#087CFA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||||
|
<rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||||
|
<polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37
|
||||||
|
29.4,33.7 26.9,33.7 26.9,22.4 "/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3
|
||||||
|
c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2
|
||||||
|
c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 353 B |
1
web_src/svg/gitea-open-with-vscodium.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |