mirror of https://github.com/usememos/memos.git
Merge cfdc67e7d6 into 8d8cc83fd8
This commit is contained in:
commit
a5ed2ce3c6
|
|
@ -405,6 +405,8 @@ message UserSetting {
|
||||||
// This references a CSS file in the web/public/themes/ directory.
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
// If not set, the default theme will be used.
|
// If not set, the default theme will be used.
|
||||||
string theme = 4 [(google.api.field_behavior) = OPTIONAL];
|
string theme = 4 [(google.api.field_behavior) = OPTIONAL];
|
||||||
|
// The user's map tile layer provider.
|
||||||
|
string map_tile_layer_provider = 5 [(google.api.field_behavior) = OPTIONAL];
|
||||||
}
|
}
|
||||||
|
|
||||||
// User authentication sessions configuration.
|
// User authentication sessions configuration.
|
||||||
|
|
|
||||||
|
|
@ -2244,9 +2244,11 @@ type UserSetting_GeneralSetting struct {
|
||||||
// The preferred theme of the user.
|
// The preferred theme of the user.
|
||||||
// This references a CSS file in the web/public/themes/ directory.
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
// If not set, the default theme will be used.
|
// If not set, the default theme will be used.
|
||||||
Theme string `protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"`
|
Theme string `protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// The user's map tile layer provider.
|
||||||
sizeCache protoimpl.SizeCache
|
MapTileLayerProvider string `protobuf:"bytes,5,opt,name=map_tile_layer_provider,json=mapTileLayerProvider,proto3" json:"map_tile_layer_provider,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserSetting_GeneralSetting) Reset() {
|
func (x *UserSetting_GeneralSetting) Reset() {
|
||||||
|
|
@ -2300,6 +2302,13 @@ func (x *UserSetting_GeneralSetting) GetTheme() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *UserSetting_GeneralSetting) GetMapTileLayerProvider() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MapTileLayerProvider
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// User authentication sessions configuration.
|
// User authentication sessions configuration.
|
||||||
type UserSetting_SessionsSetting struct {
|
type UserSetting_SessionsSetting struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
|
@ -2604,17 +2613,18 @@ const file_api_v1_user_service_proto_rawDesc = "" +
|
||||||
"\x11memos.api.v1/UserR\x04name\"\x19\n" +
|
"\x11memos.api.v1/UserR\x04name\"\x19\n" +
|
||||||
"\x17ListAllUserStatsRequest\"I\n" +
|
"\x17ListAllUserStatsRequest\"I\n" +
|
||||||
"\x18ListAllUserStatsResponse\x12-\n" +
|
"\x18ListAllUserStatsResponse\x12-\n" +
|
||||||
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xb3\a\n" +
|
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xf0\a\n" +
|
||||||
"\vUserSetting\x12\x17\n" +
|
"\vUserSetting\x12\x17\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12S\n" +
|
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12S\n" +
|
||||||
"\x0fgeneral_setting\x18\x02 \x01(\v2(.memos.api.v1.UserSetting.GeneralSettingH\x00R\x0egeneralSetting\x12V\n" +
|
"\x0fgeneral_setting\x18\x02 \x01(\v2(.memos.api.v1.UserSetting.GeneralSettingH\x00R\x0egeneralSetting\x12V\n" +
|
||||||
"\x10sessions_setting\x18\x03 \x01(\v2).memos.api.v1.UserSetting.SessionsSettingH\x00R\x0fsessionsSetting\x12c\n" +
|
"\x10sessions_setting\x18\x03 \x01(\v2).memos.api.v1.UserSetting.SessionsSettingH\x00R\x0fsessionsSetting\x12c\n" +
|
||||||
"\x15access_tokens_setting\x18\x04 \x01(\v2-.memos.api.v1.UserSetting.AccessTokensSettingH\x00R\x13accessTokensSetting\x12V\n" +
|
"\x15access_tokens_setting\x18\x04 \x01(\v2-.memos.api.v1.UserSetting.AccessTokensSettingH\x00R\x13accessTokensSetting\x12V\n" +
|
||||||
"\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x1av\n" +
|
"\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x1a\xb2\x01\n" +
|
||||||
"\x0eGeneralSetting\x12\x1b\n" +
|
"\x0eGeneralSetting\x12\x1b\n" +
|
||||||
"\x06locale\x18\x01 \x01(\tB\x03\xe0A\x01R\x06locale\x12,\n" +
|
"\x06locale\x18\x01 \x01(\tB\x03\xe0A\x01R\x06locale\x12,\n" +
|
||||||
"\x0fmemo_visibility\x18\x03 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" +
|
"\x0fmemo_visibility\x18\x03 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" +
|
||||||
"\x05theme\x18\x04 \x01(\tB\x03\xe0A\x01R\x05theme\x1aH\n" +
|
"\x05theme\x18\x04 \x01(\tB\x03\xe0A\x01R\x05theme\x12:\n" +
|
||||||
|
"\x17map_tile_layer_provider\x18\x05 \x01(\tB\x03\xe0A\x01R\x14mapTileLayerProvider\x1aH\n" +
|
||||||
"\x0fSessionsSetting\x125\n" +
|
"\x0fSessionsSetting\x125\n" +
|
||||||
"\bsessions\x18\x01 \x03(\v2\x19.memos.api.v1.UserSessionR\bsessions\x1aY\n" +
|
"\bsessions\x18\x01 \x03(\v2\x19.memos.api.v1.UserSessionR\bsessions\x1aY\n" +
|
||||||
"\x13AccessTokensSetting\x12B\n" +
|
"\x13AccessTokensSetting\x12B\n" +
|
||||||
|
|
|
||||||
|
|
@ -3459,6 +3459,9 @@ components:
|
||||||
theme:
|
theme:
|
||||||
type: string
|
type: string
|
||||||
description: "The preferred theme of the user.\r\n This references a CSS file in the web/public/themes/ directory.\r\n If not set, the default theme will be used."
|
description: "The preferred theme of the user.\r\n This references a CSS file in the web/public/themes/ directory.\r\n If not set, the default theme will be used."
|
||||||
|
mapTileLayerProvider:
|
||||||
|
type: string
|
||||||
|
description: The user's map tile layer provider.
|
||||||
description: General user settings configuration.
|
description: General user settings configuration.
|
||||||
UserSetting_SessionsSetting:
|
UserSetting_SessionsSetting:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
||||||
|
|
@ -239,9 +239,11 @@ type GeneralUserSetting struct {
|
||||||
MemoVisibility string `protobuf:"bytes,2,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
|
MemoVisibility string `protobuf:"bytes,2,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
|
||||||
// The user's theme preference.
|
// The user's theme preference.
|
||||||
// This references a CSS file in the web/public/themes/ directory.
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
Theme string `protobuf:"bytes,3,opt,name=theme,proto3" json:"theme,omitempty"`
|
Theme string `protobuf:"bytes,3,opt,name=theme,proto3" json:"theme,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// The user's map tile layer provider.
|
||||||
sizeCache protoimpl.SizeCache
|
MapTileLayerProvider string `protobuf:"bytes,4,opt,name=map_tile_layer_provider,json=mapTileLayerProvider,proto3" json:"map_tile_layer_provider,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GeneralUserSetting) Reset() {
|
func (x *GeneralUserSetting) Reset() {
|
||||||
|
|
@ -295,6 +297,13 @@ func (x *GeneralUserSetting) GetTheme() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GeneralUserSetting) GetMapTileLayerProvider() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MapTileLayerProvider
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type SessionsUserSetting struct {
|
type SessionsUserSetting struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Sessions []*SessionsUserSetting_Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"`
|
Sessions []*SessionsUserSetting_Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"`
|
||||||
|
|
@ -823,11 +832,12 @@ const file_store_user_setting_proto_rawDesc = "" +
|
||||||
"\rACCESS_TOKENS\x10\x03\x12\r\n" +
|
"\rACCESS_TOKENS\x10\x03\x12\r\n" +
|
||||||
"\tSHORTCUTS\x10\x04\x12\f\n" +
|
"\tSHORTCUTS\x10\x04\x12\f\n" +
|
||||||
"\bWEBHOOKS\x10\x05B\a\n" +
|
"\bWEBHOOKS\x10\x05B\a\n" +
|
||||||
"\x05value\"k\n" +
|
"\x05value\"\xa2\x01\n" +
|
||||||
"\x12GeneralUserSetting\x12\x16\n" +
|
"\x12GeneralUserSetting\x12\x16\n" +
|
||||||
"\x06locale\x18\x01 \x01(\tR\x06locale\x12'\n" +
|
"\x06locale\x18\x01 \x01(\tR\x06locale\x12'\n" +
|
||||||
"\x0fmemo_visibility\x18\x02 \x01(\tR\x0ememoVisibility\x12\x14\n" +
|
"\x0fmemo_visibility\x18\x02 \x01(\tR\x0ememoVisibility\x12\x14\n" +
|
||||||
"\x05theme\x18\x03 \x01(\tR\x05theme\"\xf3\x03\n" +
|
"\x05theme\x18\x03 \x01(\tR\x05theme\x125\n" +
|
||||||
|
"\x17map_tile_layer_provider\x18\x04 \x01(\tR\x14mapTileLayerProvider\"\xf3\x03\n" +
|
||||||
"\x13SessionsUserSetting\x12D\n" +
|
"\x13SessionsUserSetting\x12D\n" +
|
||||||
"\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xfd\x01\n" +
|
"\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xfd\x01\n" +
|
||||||
"\aSession\x12\x1d\n" +
|
"\aSession\x12\x1d\n" +
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ message GeneralUserSetting {
|
||||||
// The user's theme preference.
|
// The user's theme preference.
|
||||||
// This references a CSS file in the web/public/themes/ directory.
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
string theme = 3;
|
string theme = 3;
|
||||||
|
// The user's map tile layer provider.
|
||||||
|
string map_tile_layer_provider = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SessionsUserSetting {
|
message SessionsUserSetting {
|
||||||
|
|
|
||||||
|
|
@ -305,9 +305,10 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
|
||||||
|
|
||||||
func getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting {
|
func getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting {
|
||||||
return &v1pb.UserSetting_GeneralSetting{
|
return &v1pb.UserSetting_GeneralSetting{
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
MemoVisibility: "PRIVATE",
|
MemoVisibility: "PRIVATE",
|
||||||
Theme: "",
|
Theme: "",
|
||||||
|
MapTileLayerProvider: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,9 +391,10 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedGeneral := &v1pb.UserSetting_GeneralSetting{
|
updatedGeneral := &v1pb.UserSetting_GeneralSetting{
|
||||||
MemoVisibility: generalSetting.GetMemoVisibility(),
|
MemoVisibility: generalSetting.GetMemoVisibility(),
|
||||||
Locale: generalSetting.GetLocale(),
|
Locale: generalSetting.GetLocale(),
|
||||||
Theme: generalSetting.GetTheme(),
|
Theme: generalSetting.GetTheme(),
|
||||||
|
MapTileLayerProvider: generalSetting.GetMapTileLayerProvider(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply updates for fields specified in the update mask
|
// Apply updates for fields specified in the update mask
|
||||||
|
|
@ -405,6 +407,8 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
|
||||||
updatedGeneral.Theme = incomingGeneral.Theme
|
updatedGeneral.Theme = incomingGeneral.Theme
|
||||||
case "locale":
|
case "locale":
|
||||||
updatedGeneral.Locale = incomingGeneral.Locale
|
updatedGeneral.Locale = incomingGeneral.Locale
|
||||||
|
case "mapTileLayerProvider":
|
||||||
|
updatedGeneral.MapTileLayerProvider = incomingGeneral.MapTileLayerProvider
|
||||||
default:
|
default:
|
||||||
// Ignore unsupported fields
|
// Ignore unsupported fields
|
||||||
}
|
}
|
||||||
|
|
@ -1166,9 +1170,10 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32
|
||||||
if general := storeSetting.GetGeneral(); general != nil {
|
if general := storeSetting.GetGeneral(); general != nil {
|
||||||
setting.Value = &v1pb.UserSetting_GeneralSetting_{
|
setting.Value = &v1pb.UserSetting_GeneralSetting_{
|
||||||
GeneralSetting: &v1pb.UserSetting_GeneralSetting{
|
GeneralSetting: &v1pb.UserSetting_GeneralSetting{
|
||||||
Locale: general.Locale,
|
Locale: general.Locale,
|
||||||
MemoVisibility: general.MemoVisibility,
|
MemoVisibility: general.MemoVisibility,
|
||||||
Theme: general.Theme,
|
Theme: general.Theme,
|
||||||
|
MapTileLayerProvider: general.MapTileLayerProvider,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1254,9 +1259,10 @@ func convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key s
|
||||||
if general := apiSetting.GetGeneralSetting(); general != nil {
|
if general := apiSetting.GetGeneralSetting(); general != nil {
|
||||||
storeSetting.Value = &storepb.UserSetting_General{
|
storeSetting.Value = &storepb.UserSetting_General{
|
||||||
General: &storepb.GeneralUserSetting{
|
General: &storepb.GeneralUserSetting{
|
||||||
Locale: general.Locale,
|
Locale: general.Locale,
|
||||||
MemoVisibility: general.MemoVisibility,
|
MemoVisibility: general.MemoVisibility,
|
||||||
Theme: general.Theme,
|
Theme: general.Theme,
|
||||||
|
MapTileLayerProvider: general.MapTileLayerProvider,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { MapPinIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import { MapContainer, Marker, TileLayer, useMapEvents } from "react-leaflet";
|
import { MapContainer, Marker, TileLayer, useMapEvents } from "react-leaflet";
|
||||||
|
import { userStore } from "@/store";
|
||||||
|
|
||||||
const markerIcon = new DivIcon({
|
const markerIcon = new DivIcon({
|
||||||
className: "relative border-none",
|
className: "relative border-none",
|
||||||
|
|
@ -48,11 +49,22 @@ interface MapProps {
|
||||||
|
|
||||||
const DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945);
|
const DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945);
|
||||||
|
|
||||||
|
// Create a mapping for common map tile providers. If the user-supplied mapTileLayerProvider is not in the map, use it directly as the tile layer URL.
|
||||||
|
|
||||||
|
const generalSetting = userStore.state.userGeneralSetting;
|
||||||
|
|
||||||
const LeafletMap = (props: MapProps) => {
|
const LeafletMap = (props: MapProps) => {
|
||||||
const position = props.latlng || DEFAULT_CENTER_LAT_LNG;
|
const position = props.latlng || DEFAULT_CENTER_LAT_LNG;
|
||||||
|
// Default to OpenStreetMap if not set, otherwise use the set value as the URL
|
||||||
|
let tileLayerUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||||
|
const tileLayerProvider = generalSetting?.mapTileLayerProvider;
|
||||||
|
if (tileLayerProvider && tileLayerProvider.trim() !== "") {
|
||||||
|
tileLayerUrl = tileLayerProvider;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapContainer className="w-full h-72" center={position} zoom={13} scrollWheelZoom={false}>
|
<MapContainer className="w-full h-72" center={position} zoom={13} scrollWheelZoom={false}>
|
||||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
<TileLayer url={tileLayerUrl} />
|
||||||
<LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => {}} />
|
<LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => {}} />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
import { ExternalLinkIcon, Settings2Icon } from "lucide-react";
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内置地图模板配置
|
||||||
|
const BUILTIN_TEMPLATES = {
|
||||||
|
openstreetmap: {
|
||||||
|
name: "OpenStreetMap",
|
||||||
|
url: "",
|
||||||
|
wiki: "https://wiki.openstreetmap.org/wiki/Tiles",
|
||||||
|
requiresToken: false,
|
||||||
|
},
|
||||||
|
cartodb: {
|
||||||
|
name: "CartoDB",
|
||||||
|
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
||||||
|
wiki: "https://carto.com/help/building-maps/basemap-list/",
|
||||||
|
requiresToken: false,
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
name: "Google Maps",
|
||||||
|
url: "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key={your_token}",
|
||||||
|
wiki: "https://developers.google.com/maps/documentation/tile/overview",
|
||||||
|
requiresToken: true,
|
||||||
|
},
|
||||||
|
apple: {
|
||||||
|
name: "Apple Maps",
|
||||||
|
url: "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
wiki: "https://developer.apple.com/maps/",
|
||||||
|
requiresToken: true,
|
||||||
|
},
|
||||||
|
bing: {
|
||||||
|
name: "Bing Maps",
|
||||||
|
url: "https://ecn.t3.tiles.virtualearth.net/tiles/a{q}.jpeg?g=1&key={your_token}",
|
||||||
|
wiki: "https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata",
|
||||||
|
requiresToken: true,
|
||||||
|
},
|
||||||
|
mapbox: {
|
||||||
|
name: "Mapbox",
|
||||||
|
url: "https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token={your_token}",
|
||||||
|
wiki: "https://docs.mapbox.com/api/maps/",
|
||||||
|
requiresToken: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapTileLayerProviderSelect = (props: Props) => {
|
||||||
|
const { value, onValueChange, className } = props;
|
||||||
|
const [isCustomDialogOpen, setIsCustomDialogOpen] = useState(false);
|
||||||
|
const [customUrl, setCustomUrl] = useState("");
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setIsCustomDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomUrlSubmit = () => {
|
||||||
|
onValueChange(customUrl.trim());
|
||||||
|
setIsCustomDialogOpen(false);
|
||||||
|
setCustomUrl("");
|
||||||
|
setSelectedTemplate("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTemplateSelect = (templateKey: string) => {
|
||||||
|
const template = BUILTIN_TEMPLATES[templateKey as keyof typeof BUILTIN_TEMPLATES];
|
||||||
|
if (template) {
|
||||||
|
setCustomUrl(template.url);
|
||||||
|
setSelectedTemplate(templateKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUrlChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setCustomUrl(newValue);
|
||||||
|
|
||||||
|
// If user manually modifies the URL, clear the template selection
|
||||||
|
if (selectedTemplate) {
|
||||||
|
const template = BUILTIN_TEMPLATES[selectedTemplate as keyof typeof BUILTIN_TEMPLATES];
|
||||||
|
if (template && newValue !== template.url) {
|
||||||
|
setSelectedTemplate("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-resize input based on content
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
const input = inputRef.current;
|
||||||
|
input.style.height = "auto";
|
||||||
|
input.style.height = `${input.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}, [customUrl]);
|
||||||
|
|
||||||
|
const getDisplayName = () => {
|
||||||
|
if (value === "") return "OpenStreetMap";
|
||||||
|
if (Object.keys(BUILTIN_TEMPLATES).includes(value)) {
|
||||||
|
const template = BUILTIN_TEMPLATES[value as keyof typeof BUILTIN_TEMPLATES];
|
||||||
|
return template ? template.name : "Custom";
|
||||||
|
}
|
||||||
|
return "Custom";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={handleEditClick} className={className || "min-w-fit justify-between"}>
|
||||||
|
<span>{getDisplayName()}</span>
|
||||||
|
<Settings2Icon className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog open={isCustomDialogOpen} onOpenChange={setIsCustomDialogOpen}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("setting.preference-section.map-config.title")}</DialogTitle>
|
||||||
|
<DialogDescription>{t("setting.preference-section.map-config.description")}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>{t("setting.preference-section.map-config.quick-templates")}</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{Object.entries(BUILTIN_TEMPLATES).map(([key, template]) => (
|
||||||
|
<Button
|
||||||
|
key={key}
|
||||||
|
variant={selectedTemplate === key ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTemplateSelect(key)}
|
||||||
|
className="justify-start text-left h-auto py-3 px-3"
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="font-medium">{template.name}</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(template.wiki, "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{template.requiresToken && (
|
||||||
|
<div className="text-xs text-orange-600 mt-1">{t("setting.preference-section.map-config.requires-api-key")}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="custom-url">{t("setting.preference-section.map-config.tile-server-url")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id="custom-url"
|
||||||
|
placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
value={customUrl}
|
||||||
|
onChange={handleUrlChange}
|
||||||
|
className="mt-2 min-h-[40px] resize-none"
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-muted p-3 rounded-md">
|
||||||
|
<h4 className="font-medium mb-2">{t("setting.preference-section.map-config.parameters")}</h4>
|
||||||
|
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<code className="bg-background px-1 rounded">{"{z}"}</code> {t("setting.preference-section.map-config.zoom-level")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code className="bg-background px-1 rounded">{"{x}"}</code> {t("setting.preference-section.map-config.tile-x-coordinate")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code className="bg-background px-1 rounded">{"{y}"}</code> {t("setting.preference-section.map-config.tile-y-coordinate")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code className="bg-background px-1 rounded">{"{s}"}</code> {t("setting.preference-section.map-config.subdomain")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code className="bg-background px-1 rounded">{"{r}"}</code> {t("setting.preference-section.map-config.retina-resolution")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code className="bg-background px-1 rounded">{"{your_token}"}</code>{" "}
|
||||||
|
{t("setting.preference-section.map-config.api-token-placeholder")}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsCustomDialogOpen(false)}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCustomUrlSubmit}>{t("setting.preference-section.map-config.apply")}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapTileLayerProviderSelect;
|
||||||
|
|
@ -7,6 +7,7 @@ import { UserSetting_GeneralSetting } from "@/types/proto/api/v1/user_service";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo";
|
import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo";
|
||||||
import LocaleSelect from "../LocaleSelect";
|
import LocaleSelect from "../LocaleSelect";
|
||||||
|
import MapTileLayerProviderSelect from "../MapTileLayerProviderSelect";
|
||||||
import ThemeSelect from "../ThemeSelect";
|
import ThemeSelect from "../ThemeSelect";
|
||||||
import VisibilityIcon from "../VisibilityIcon";
|
import VisibilityIcon from "../VisibilityIcon";
|
||||||
import WebhookSection from "./WebhookSection";
|
import WebhookSection from "./WebhookSection";
|
||||||
|
|
@ -27,11 +28,16 @@ const PreferencesSection = observer(() => {
|
||||||
await userStore.updateUserGeneralSetting({ theme }, ["theme"]);
|
await userStore.updateUserGeneralSetting({ theme }, ["theme"]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMapApiProviderChange = async (mapApiProvider: string) => {
|
||||||
|
await userStore.updateUserGeneralSetting({ mapTileLayerProvider: mapApiProvider }, ["mapTileLayerProvider"]);
|
||||||
|
};
|
||||||
|
|
||||||
// Provide default values if setting is not loaded yet
|
// Provide default values if setting is not loaded yet
|
||||||
const setting: UserSetting_GeneralSetting = generalSetting || {
|
const setting: UserSetting_GeneralSetting = generalSetting || {
|
||||||
locale: "en",
|
locale: "en",
|
||||||
memoVisibility: "PRIVATE",
|
memoVisibility: "PRIVATE",
|
||||||
theme: "default",
|
theme: "default",
|
||||||
|
mapTileLayerProvider: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -71,6 +77,11 @@ const PreferencesSection = observer(() => {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-row justify-between items-center mt-2">
|
||||||
|
<span className="truncate">{t("setting.preference-section.map-tile-layer-provider")}</span>
|
||||||
|
<MapTileLayerProviderSelect value={setting.mapTileLayerProvider} onValueChange={handleMapApiProviderChange} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator className="my-3" />
|
<Separator className="my-3" />
|
||||||
|
|
||||||
<WebhookSection />
|
<WebhookSection />
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,24 @@
|
||||||
"preference-section": {
|
"preference-section": {
|
||||||
"default-memo-sort-option": "Memo display time",
|
"default-memo-sort-option": "Memo display time",
|
||||||
"default-memo-visibility": "Default memo visibility",
|
"default-memo-visibility": "Default memo visibility",
|
||||||
"theme": "Theme"
|
"theme": "Theme",
|
||||||
|
"map-tile-layer-provider": "Map Tile Layer Provider",
|
||||||
|
"map-tile-layer-provider-custom-url": "Custom Map Tile Layer Provider URL",
|
||||||
|
"map-config": {
|
||||||
|
"title": "Map Tile Server Configuration",
|
||||||
|
"description": "Choose a built-in template or enter a custom URL template. Use Leaflet placeholders like {z}, {x}, {y} for zoom level and tile coordinates. Replace <your_token> with your actual API key when needed.",
|
||||||
|
"quick-templates": "Quick Templates",
|
||||||
|
"tile-server-url": "Tile Server URL Template",
|
||||||
|
"parameters": "Leaflet URL Template Parameters:",
|
||||||
|
"zoom-level": "Zoom level (0-18)",
|
||||||
|
"tile-x-coordinate": "Tile X coordinate",
|
||||||
|
"tile-y-coordinate": "Tile Y coordinate",
|
||||||
|
"subdomain": "Subdomain (a, b, c) for load balancing",
|
||||||
|
"retina-resolution": "Retina resolution (@2x)",
|
||||||
|
"api-token-placeholder": "API token/key placeholder (edit directly in URL)",
|
||||||
|
"requires-api-key": "Requires API Key",
|
||||||
|
"apply": "Apply"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sso": "SSO",
|
"sso": "SSO",
|
||||||
"sso-section": {
|
"sso-section": {
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,24 @@
|
||||||
"preference-section": {
|
"preference-section": {
|
||||||
"default-memo-sort-option": "备忘录显示时间",
|
"default-memo-sort-option": "备忘录显示时间",
|
||||||
"default-memo-visibility": "默认备忘录可见性",
|
"default-memo-visibility": "默认备忘录可见性",
|
||||||
"theme": "主题"
|
"theme": "主题",
|
||||||
|
"map-tile-layer-provider": "地图图层提供商",
|
||||||
|
"map-tile-layer-provider-custom-url": "自定义地图图层提供商URL",
|
||||||
|
"map-config": {
|
||||||
|
"title": "地图图层服务器配置",
|
||||||
|
"description": "选择内置模板或输入自定义URL模板。使用Leaflet占位符如 {z}、{x}、{y} 表示缩放级别和图块坐标。需要时请将 <your_token> 替换为您的实际API密钥。",
|
||||||
|
"quick-templates": "快速模板",
|
||||||
|
"tile-server-url": "图层服务器URL模板",
|
||||||
|
"parameters": "Leaflet URL模板参数:",
|
||||||
|
"zoom-level": "缩放级别 (0-18)",
|
||||||
|
"tile-x-coordinate": "图块X坐标",
|
||||||
|
"tile-y-coordinate": "图块Y坐标",
|
||||||
|
"subdomain": "子域名 (a, b, c) 用于负载均衡",
|
||||||
|
"retina-resolution": "视网膜分辨率 (@2x)",
|
||||||
|
"api-token-placeholder": "API令牌/密钥占位符(直接在URL中编辑)",
|
||||||
|
"requires-api-key": "需要API密钥",
|
||||||
|
"apply": "应用"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sso": "单点登录",
|
"sso": "单点登录",
|
||||||
"sso-section": {
|
"sso-section": {
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,8 @@ export interface UserSetting_GeneralSetting {
|
||||||
* If not set, the default theme will be used.
|
* If not set, the default theme will be used.
|
||||||
*/
|
*/
|
||||||
theme: string;
|
theme: string;
|
||||||
|
/** The user's map tile layer provider. */
|
||||||
|
mapTileLayerProvider: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** User authentication sessions configuration. */
|
/** User authentication sessions configuration. */
|
||||||
|
|
@ -1718,7 +1720,7 @@ export const UserSetting: MessageFns<UserSetting> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function createBaseUserSetting_GeneralSetting(): UserSetting_GeneralSetting {
|
function createBaseUserSetting_GeneralSetting(): UserSetting_GeneralSetting {
|
||||||
return { locale: "", memoVisibility: "", theme: "" };
|
return { locale: "", memoVisibility: "", theme: "", mapTileLayerProvider: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserSetting_GeneralSetting: MessageFns<UserSetting_GeneralSetting> = {
|
export const UserSetting_GeneralSetting: MessageFns<UserSetting_GeneralSetting> = {
|
||||||
|
|
@ -1732,6 +1734,9 @@ export const UserSetting_GeneralSetting: MessageFns<UserSetting_GeneralSetting>
|
||||||
if (message.theme !== "") {
|
if (message.theme !== "") {
|
||||||
writer.uint32(34).string(message.theme);
|
writer.uint32(34).string(message.theme);
|
||||||
}
|
}
|
||||||
|
if (message.mapTileLayerProvider !== "") {
|
||||||
|
writer.uint32(42).string(message.mapTileLayerProvider);
|
||||||
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1766,6 +1771,14 @@ export const UserSetting_GeneralSetting: MessageFns<UserSetting_GeneralSetting>
|
||||||
message.theme = reader.string();
|
message.theme = reader.string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.mapTileLayerProvider = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -1783,6 +1796,7 @@ export const UserSetting_GeneralSetting: MessageFns<UserSetting_GeneralSetting>
|
||||||
message.locale = object.locale ?? "";
|
message.locale = object.locale ?? "";
|
||||||
message.memoVisibility = object.memoVisibility ?? "";
|
message.memoVisibility = object.memoVisibility ?? "";
|
||||||
message.theme = object.theme ?? "";
|
message.theme = object.theme ?? "";
|
||||||
|
message.mapTileLayerProvider = object.mapTileLayerProvider ?? "";
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue