diff --git a/src/main.ts b/src/main.ts index 8d190c8..b7c2f3c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { App, Notice, Plugin, PluginSettingTab, requestUrl, TFile, TFolder } from "obsidian"; +import { App, Notice, Platform, Plugin, PluginSettingTab, requestUrl, TFile, TFolder } from "obsidian"; import { createApp, reactive, type App as VueApp } from "vue"; import { createRandomId, createVaultId, normalizeKey } from "./core/ids"; import SettingsApp from "./settings/SettingsApp.vue"; @@ -6,6 +6,8 @@ import "./styles.scss"; import { syncOnce, type LocalSyncState, type VaultIO } from "./sync/engine"; import { S3ObjectStore } from "./store/s3"; +declare const require: ((id: string) => unknown) | undefined; + interface ObsyncSettings { endpoint: string; bucket: string; @@ -160,14 +162,14 @@ export default class ObsyncPlugin extends Plugin { this.settings.vaultKey = normalizeKey(this.app.vault.getName()); } - if (!this.settings.deviceName) { - this.settings.deviceName = "This device"; - } - if (!this.settings.deviceId) { this.settings.deviceId = createRandomId("dev"); } + if (!this.settings.deviceName || this.settings.deviceName === "This device") { + this.settings.deviceName = getCurrentDeviceName(this.settings.deviceId); + } + if (!this.settings.syncState) { this.settings.syncState = { files: {} }; } @@ -215,6 +217,38 @@ export default class ObsyncPlugin extends Plugin { } } +function getCurrentDeviceName(deviceId: string): string { + try { + if (typeof require !== "function") { + return getPlatformDeviceName(deviceId); + } + + const os = require("node:os") as { hostname?: () => string }; + return os.hostname?.().trim() || getPlatformDeviceName(deviceId); + } catch { + return getPlatformDeviceName(deviceId); + } +} + +function getPlatformDeviceName(deviceId: string): string { + const suffix = getDeviceNameSuffix(deviceId); + + if (Platform.isIosApp) { + return `${Platform.isTablet ? "iPad" : "iPhone"} ${suffix}`; + } + + if (Platform.isAndroidApp) { + return `${Platform.isTablet ? "Android Tablet" : "Android Phone"} ${suffix}`; + } + + return `Desktop ${suffix}`; +} + +function getDeviceNameSuffix(deviceId: string): string { + const compactId = deviceId.replace(/^dev_/, ""); + return compactId.slice(-4) || "0000"; +} + function parseConnectionConfig(configText: string): ConnectionConfig { let parsed: unknown; @@ -374,16 +408,17 @@ class ObsyncSettingTab extends PluginSettingTab { await this.plugin.updateSettings(update); Object.assign(connectionConfig, this.plugin.getConnectionConfig()); }, - onCopyVaultId: async () => { - await navigator.clipboard.writeText(this.plugin.settings.vaultId); - new Notice("Vault ID 已复制。"); + onCopyDeviceId: async () => { + await navigator.clipboard.writeText(this.plugin.settings.deviceId); + new Notice("设备 ID 已复制。"); }, onCopyConnectionConfig: async () => { await navigator.clipboard.writeText(JSON.stringify(this.plugin.getConnectionConfig(), null, 2)); new Notice("连接配置已复制。"); }, - onImportConnectionConfig: async (configText: string) => { + onPasteConnectionConfig: async () => { try { + const configText = await navigator.clipboard.readText(); await this.plugin.importConnectionConfig(configText); Object.assign(settings, this.plugin.settings); Object.assign(connectionConfig, this.plugin.getConnectionConfig()); diff --git a/src/settings/SettingsApp.vue b/src/settings/SettingsApp.vue index a643e0e..4af1c27 100644 --- a/src/settings/SettingsApp.vue +++ b/src/settings/SettingsApp.vue @@ -25,14 +25,13 @@ const props = defineProps<{ const emit = defineEmits<{ update: [update: Partial]; - copyVaultId: []; + copyDeviceId: []; copyConnectionConfig: []; - importConnectionConfig: [configText: string]; + pasteConnectionConfig: []; }>(); const showSecretAccessKey = ref(false); const secretAccessKeyButton = ref(null); -const importConfigText = ref(""); watchEffect(() => { const button = secretAccessKeyButton.value; @@ -186,7 +185,6 @@ watchEffect(() => {
{{ props.settings.vaultId }} -
@@ -194,7 +192,12 @@ watchEffect(() => {
-

设备名称

+
+

设备名称

+ +

用于冲突文件名和界面展示。

@@ -205,16 +208,6 @@ watchEffect(() => {
-
-
-

设备 ID

-

首次启用插件时随机生成。

-
-
- {{ props.settings.deviceId }} -
-
-

自动同步

@@ -238,24 +231,17 @@ watchEffect(() => {

复制到另一台设备导入。这里不包含 Access Key 和 Secret。

-
{{ JSON.stringify(props.connectionConfig, null, 2) }}
+
{{ JSON.stringify(props.connectionConfig, null, 2) }}
- +
+ - - - + +
diff --git a/src/styles.scss b/src/styles.scss index 6520018..bdc6bca 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -44,12 +44,20 @@ } .obsync-row { - @apply flex items-center justify-between gap-5 px-5 py-4; - border-bottom: 1px solid var(--background-modifier-border); + @apply relative flex items-center justify-between gap-5 px-5 py-4; } -.obsync-row:last-child { - @apply border-b-0; +.obsync-row::after { + content: ""; + position: absolute; + inset-inline: 1.25rem; + bottom: 0; + height: 1px; + background: var(--background-modifier-border); +} + +.obsync-row:last-child::after { + display: none; } .obsync-row-copy { @@ -73,6 +81,23 @@ } } +.obsync-row-title { + @apply flex items-center justify-between gap-3; +} + +.obsync-link-button { + @apply shrink-0 cursor-pointer rounded px-2 py-1 text-xs font-semibold leading-4 transition; + background: transparent; + border: 0; + box-shadow: none; + color: var(--text-muted); +} + +.obsync-link-button:hover { + background: var(--background-modifier-hover); + color: var(--text-normal); +} + .obsync-control { @apply w-60 shrink-0; } @@ -157,7 +182,6 @@ color: var(--text-normal); } -.obsync-readonly button, .obsync-primary { @apply shrink-0 cursor-pointer rounded-md border-0 px-3 py-2 text-sm font-semibold leading-5 transition; background: var(--interactive-accent); @@ -171,13 +195,11 @@ color: var(--text-normal); } -.obsync-readonly button:hover, .obsync-primary:hover, .obsync-secondary:hover { @apply opacity-90; } -.obsync-readonly button:active, .obsync-primary:active, .obsync-secondary:active { @apply translate-y-px; @@ -218,28 +240,25 @@ @apply mb-3; } -.obsync-row-block pre { - @apply mb-3 max-h-60 overflow-auto rounded-md p-3 text-xs leading-5; +.obsync-config-display { + @apply min-h-48 rounded-md p-3 font-mono text-xs leading-5; background: var(--background-primary); + border: 1px solid var(--background-modifier-border); color: var(--text-normal); overflow-wrap: anywhere; white-space: pre-wrap; } -.obsync-row-block textarea { - @apply mt-3 h-28 w-full resize-y rounded-md p-3 text-xs leading-5 outline-none transition; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - color: var(--text-normal); +.obsync-action-row { + @apply mt-3 flex gap-2; } -.obsync-row-block textarea:focus { - border-color: var(--interactive-accent); - box-shadow: 0 0 0 2px var(--background-modifier-border-focus); +.obsync-action-row button { + @apply flex-1; } -.obsync-row-block textarea::placeholder { - color: var(--text-faint); +.obsync-action-row .obsync-secondary { + @apply mt-0; } @container (max-width: 640px) { @@ -255,10 +274,18 @@ @apply block px-4 py-4; } + .obsync-row::after { + inset-inline: 1rem; + } + .obsync-row-copy { @apply mb-2; } + .obsync-row-title { + @apply items-start; + } + .obsync-control, .obsync-control-wide, .obsync-readonly { @@ -273,7 +300,6 @@ @apply text-left; } - .obsync-readonly button, .obsync-primary, .obsync-secondary { @apply w-full;