Merge pull request 'Replace "configurable clone methods" with Gitea's more flexible implementation' (#2740) from algernon/forgejo:gitea/port/repo-open-with into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2740 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
|
@ -292,9 +292,6 @@ package "code.gitea.io/gitea/modules/translation"
|
|||
func (MockLocale).TrN
|
||||
func (MockLocale).PrettyNumber
|
||||
|
||||
package "code.gitea.io/gitea/modules/util"
|
||||
func UnsafeStringToBytes
|
||||
|
||||
package "code.gitea.io/gitea/modules/util/filebuffer"
|
||||
func CreateFromReader
|
||||
|
||||
|
|
|
@ -15,8 +15,45 @@ type PictureStruct struct {
|
|||
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 {
|
||||
Picture *PictureStruct
|
||||
Repository *RepositoryStruct
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -28,8 +65,11 @@ func initDefaultConfig() {
|
|||
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
|
||||
defaultConfig = &ConfigStruct{
|
||||
Picture: &PictureStruct{
|
||||
DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
|
||||
EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
|
||||
DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
|
||||
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{}
|
||||
|
||||
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
|
||||
if key == "" {
|
||||
return "", false
|
||||
}
|
||||
cfgSec, err := CfgProvider.GetSection(sec)
|
||||
if err != nil {
|
||||
log.Error("Unable to get config section: %q", sec)
|
||||
|
|
|
@ -5,8 +5,11 @@ package config
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type CfgSecKey struct {
|
||||
|
@ -23,15 +26,15 @@ type Value[T any] struct {
|
|||
revision int
|
||||
}
|
||||
|
||||
func (value *Value[T]) parse(s string) (v T) {
|
||||
switch any(v).(type) {
|
||||
case bool:
|
||||
b, _ := strconv.ParseBool(s)
|
||||
return any(b).(T)
|
||||
default:
|
||||
panic("unsupported config type, please complete the code")
|
||||
func (value *Value[T]) parse(key, valStr string) (v T) {
|
||||
v = value.def
|
||||
if valStr != "" {
|
||||
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
|
||||
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||
dg := GetDynGetter()
|
||||
|
@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
|
|||
if valStr == nil {
|
||||
v = value.def
|
||||
} else {
|
||||
v = value.parse(*valStr)
|
||||
v = value.parse(value.dynKey, *valStr)
|
||||
}
|
||||
|
||||
value.mu.Lock()
|
||||
|
@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
|
|||
return value.dynKey
|
||||
}
|
||||
|
||||
func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
|
||||
return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
|
||||
func (value *Value[T]) WithDefault(def T) *Value[T] {
|
||||
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}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -20,8 +19,6 @@ const (
|
|||
RepoCreatingPublic = "public"
|
||||
)
|
||||
|
||||
var RecognisedRepositoryDownloadOrCloneMethods = []string{"download-zip", "download-targz", "download-bundle", "vscode-clone", "vscodium-clone", "cite"}
|
||||
|
||||
// MaxUserCardsPerPage sets maximum amount of watchers and stargazers shown per page
|
||||
// those pages use 2 or 3 column layout, so the value should be divisible by 2 and 3
|
||||
var MaxUserCardsPerPage = 36
|
||||
|
@ -50,7 +47,6 @@ var (
|
|||
DisabledRepoUnits []string
|
||||
DefaultRepoUnits []string
|
||||
DefaultForkRepoUnits []string
|
||||
DownloadOrCloneMethods []string
|
||||
PrefixArchiveFiles bool
|
||||
DisableMigrations bool
|
||||
DisableStars bool
|
||||
|
@ -173,7 +169,6 @@ var (
|
|||
DisabledRepoUnits: []string{},
|
||||
DefaultRepoUnits: []string{},
|
||||
DefaultForkRepoUnits: []string{},
|
||||
DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"},
|
||||
PrefixArchiveFiles: true,
|
||||
DisableMigrations: false,
|
||||
DisableStars: false,
|
||||
|
@ -377,12 +372,5 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
|||
if err := loadRepoArchiveFrom(rootCfg); err != nil {
|
||||
log.Fatal("loadRepoArchiveFrom: %v", err)
|
||||
}
|
||||
|
||||
for _, method := range Repository.DownloadOrCloneMethods {
|
||||
if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) {
|
||||
log.Error("Unrecognised repository download or clone method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
|
||||
}
|
||||
|
|
|
@ -53,7 +53,6 @@ func CommonTemplateContextData() ContextData {
|
|||
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
|
||||
"ShowFooterVersion": setting.Other.ShowFooterVersion,
|
||||
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
|
||||
"DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods,
|
||||
|
||||
"EnableSwagger": setting.API.EnableSwagger,
|
||||
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
|
||||
|
|
|
@ -1015,8 +1015,7 @@ fork_branch = Branch to be cloned to the fork
|
|||
all_branches = All branches
|
||||
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
||||
use_template = Use this template
|
||||
clone_in_vsc = Clone in VS Code
|
||||
clone_in_vscodium = Clone in VSCodium
|
||||
open_with_editor = Open with %s
|
||||
download_zip = Download ZIP
|
||||
download_tar = Download TAR.GZ
|
||||
download_bundle = Download BUNDLE
|
||||
|
@ -2833,6 +2832,8 @@ authentication = Authentication sources
|
|||
emails = User emails
|
||||
config = Configuration
|
||||
notices = System notices
|
||||
config_summary = Summary
|
||||
config_settings = Settings
|
||||
monitor = Monitoring
|
||||
first_page = First
|
||||
last_page = Last
|
||||
|
@ -3271,6 +3272,7 @@ config.picture_config = Picture and avatar configuration
|
|||
config.picture_service = Picture service
|
||||
config.disable_gravatar = Disable Gravatar
|
||||
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_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 (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -24,7 +24,10 @@ import (
|
|||
"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
|
||||
func SendTestMail(ctx *context.Context) {
|
||||
|
@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
|
|||
|
||||
// Config show admin config page
|
||||
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["PageIsAdminConfigSummary"] = true
|
||||
|
||||
ctx.Data["CustomConf"] = setting.CustomConf
|
||||
ctx.Data["AppUrl"] = setting.AppURL
|
||||
|
@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
|
|||
|
||||
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Data["SystemConfig"] = setting.Config()
|
||||
prepareDeprecatedWarningsAlert(ctx)
|
||||
|
||||
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) {
|
||||
key := strings.TrimSpace(ctx.FormString("key"))
|
||||
value := ctx.FormString("value")
|
||||
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))
|
||||
return
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
|
||||
log.Error("set setting failed: %v", err)
|
||||
marshaledValue, err := marshaller(value)
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import (
|
|||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
|
@ -812,7 +813,7 @@ func Home(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
renderCode(ctx)
|
||||
renderHomeCode(ctx)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
|
||||
prepareOpenWithEditorApps(ctx)
|
||||
|
||||
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
|
||||
showEmpty := true
|
||||
|
|
|
@ -691,6 +691,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("", admin.Config)
|
||||
m.Post("", admin.ChangeConfig)
|
||||
m.Post("/test_mail", admin.SendTestMail)
|
||||
m.Get("/settings", admin.ConfigSettings)
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
|
|
|
@ -192,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
ctx.Data["SystemConfig"] = setting.Config()
|
||||
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
|
||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
|
||||
|
||||
|
|
|
@ -285,27 +285,6 @@
|
|||
</dl>
|
||||
</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">
|
||||
{{ctx.Locale.Tr "admin.config.git_config"}}
|
||||
</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>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsAdminConfig}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
|
||||
{{ctx.Locale.Tr "admin.config"}}
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
|
||||
<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">
|
||||
{{ctx.Locale.Tr "admin.notices"}}
|
||||
</a>
|
||||
|
|
|
@ -43,11 +43,8 @@
|
|||
for (const el of document.getElementsByClassName('js-clone-url')) {
|
||||
el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
|
||||
}
|
||||
for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
|
||||
el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link);
|
||||
}
|
||||
for (const el of document.getElementsByClassName('js-clone-url-vscodium')) {
|
||||
el['href'] = 'vscodium://vscode.git/clone?url=' + encodeURIComponent(link);
|
||||
for (const el of document.getElementsByClassName('js-clone-url-editor')) {
|
||||
el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
|
@ -137,31 +137,16 @@
|
|||
<button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
<div class="menu">
|
||||
{{$citation := .CitationExist}}
|
||||
{{$originLink := .CloneButtonOriginLink}}
|
||||
{{range $.DownloadOrCloneMethods}}
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
{{if eq . "download-zip"}}
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
|
||||
{{end}}
|
||||
{{if eq . "download-targz"}}
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
|
||||
{{end}}
|
||||
{{if eq . "download-bundle"}}
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
|
||||
{{end}}
|
||||
{{if $citation}}
|
||||
{{if eq . "cite"}}
|
||||
{{if .CitiationExist}}
|
||||
<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}}
|
||||
{{if eq . "vscode-clone"}}
|
||||
<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{$originLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a>
|
||||
{{end}}
|
||||
{{if eq . "vscodium-clone"}}
|
||||
<a class="item js-clone-url-vscodium" href="vscodium://vscode.git/clone?url={{$originLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vscodium"}}</a>
|
||||
{{end}}
|
||||
{{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>
|
||||
</button>
|
||||
|
|
|
@ -54,84 +54,6 @@ func TestViewRepo(t *testing.T) {
|
|||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestViewRepoCloneMethods(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
getCloneMethods := func() []string {
|
||||
req := NewRequest(t, "GET", "/user2/repo1")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
cloneMoreMethodsHTML := htmlDoc.doc.Find("#more-btn div a")
|
||||
|
||||
var methods []string
|
||||
cloneMoreMethodsHTML.Each(func(i int, s *goquery.Selection) {
|
||||
a, _ := s.Attr("href")
|
||||
methods = append(methods, a)
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
testCloneMethods := func(expected []string) {
|
||||
methods := getCloneMethods()
|
||||
|
||||
assert.Len(t, methods, len(expected))
|
||||
for i, expectedMethod := range expected {
|
||||
assert.Contains(t, methods[i], expectedMethod)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
testCloneMethods([]string{"/master.zip", "/master.tar.gz", "/master.bundle", "vscode://"})
|
||||
})
|
||||
|
||||
t.Run("Customized methods", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{"vscodium-clone", "download-targz"})()
|
||||
|
||||
testCloneMethods([]string{"vscodium://", "/master.tar.gz"})
|
||||
})
|
||||
|
||||
t.Run("Individual methods", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
singleMethodTest := func(method, expectedURLPart string) {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{method})()
|
||||
|
||||
testCloneMethods([]string{expectedURLPart})
|
||||
})
|
||||
}
|
||||
|
||||
cases := map[string]string{
|
||||
"download-zip": "/master.zip",
|
||||
"download-targz": "/master.tar.gz",
|
||||
"download-bundle": "/master.bundle",
|
||||
"vscode-clone": "vscode://",
|
||||
"vscodium-clone": "vscodium://",
|
||||
}
|
||||
for method, expectedURLPart := range cases {
|
||||
singleMethodTest(method, expectedURLPart)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All methods", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, setting.RecognisedRepositoryDownloadOrCloneMethods)()
|
||||
|
||||
methods := getCloneMethods()
|
||||
// We compare against
|
||||
// len(setting.RecognisedRepositoryDownloadOrCloneMethods) - 1, because
|
||||
// the test environment does not currently set things up for the cite
|
||||
// method to display.
|
||||
assert.GreaterOrEqual(t, len(methods), len(setting.RecognisedRepositoryDownloadOrCloneMethods)-1)
|
||||
})
|
||||
}
|
||||
|
||||
func testViewRepo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
@ -1012,3 +934,64 @@ func TestRepoFollowSymlink(t *testing.T) {
|
|||
assertCase(t, "/user2/readme-test/src/branch/master/README.md", "", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestViewRepoOpenWith(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
getOpenWith := func() []string {
|
||||
req := NewRequest(t, "GET", "/user2/repo1")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
openWithHTML := htmlDoc.doc.Find(".js-clone-url-editor")
|
||||
|
||||
var methods []string
|
||||
openWithHTML.Each(func(i int, s *goquery.Selection) {
|
||||
a, _ := s.Attr("data-href-template")
|
||||
methods = append(methods, a)
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
testOpenWith := func(expected []string) {
|
||||
methods := getOpenWith()
|
||||
|
||||
assert.Len(t, methods, len(expected))
|
||||
for i, expectedMethod := range expected {
|
||||
assert.True(t, strings.HasPrefix(methods[i], expectedMethod))
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
testOpenWith([]string{"vscode://", "vscodium://", "jetbrains://"})
|
||||
})
|
||||
|
||||
t.Run("Customised", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Change the methods via the admin settings
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
setEditorApps := func(t *testing.T, apps string) {
|
||||
t.Helper()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/admin/config?key=repository.open-with.editor-apps", map[string]string{
|
||||
"value": apps,
|
||||
"_csrf": GetCSRF(t, session, "/admin/config/settings"),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
setEditorApps(t, "")
|
||||
}()
|
||||
|
||||
setEditorApps(t, "test = test://?url={url}")
|
||||
|
||||
testOpenWith([]string{"test://"})
|
||||
})
|
||||
}
|
||||
|
|
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 |