From 67a506dc669dd44d1cc88bf57069bb6353e52336 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 10 Aug 2025 16:28:08 -0700 Subject: [PATCH] Draggable. --- Sources/Secretive/Localizable.xcstrings | 128 --------------------- Sources/Secretive/Views/CopyableView.swift | 96 +++++++++++----- 2 files changed, 69 insertions(+), 155 deletions(-) diff --git a/Sources/Secretive/Localizable.xcstrings b/Sources/Secretive/Localizable.xcstrings index 3dcedc8..7cdd6c9 100644 --- a/Sources/Secretive/Localizable.xcstrings +++ b/Sources/Secretive/Localizable.xcstrings @@ -1103,134 +1103,6 @@ } } }, - "copyable_click_to_copy_button" : { - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clica per copiar" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zum Kopieren Anklicken" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to Copy" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Klikkaa kopioidaksesi" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cliquer pour copier" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clicca per copiare" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "クリックしてコピー" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "복사하기" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clique para Copiar" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点击拷贝" - } - } - } - }, - "copyable_copied" : { - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copiat" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopiert" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copied" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopioitu" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copié" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copiato" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "コピーしました" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "복사됨" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copiado" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "已拷贝" - } - } - } - }, "create_secret_cancel_button" : { "localizations" : { "ca" : { diff --git a/Sources/Secretive/Views/CopyableView.swift b/Sources/Secretive/Views/CopyableView.swift index 8b2630f..1ca161e 100644 --- a/Sources/Secretive/Views/CopyableView.swift +++ b/Sources/Secretive/Views/CopyableView.swift @@ -8,9 +8,9 @@ struct CopyableView: View { var text: String @State private var interactionState: InteractionState = .normal - @Environment(\.colorScheme) private var colorScheme - - var body: some View { + @Namespace var namespace + + var content: some View { VStack(alignment: .leading) { HStack { image @@ -22,7 +22,7 @@ struct CopyableView: View { .foregroundColor(primaryTextColor) Spacer() if interactionState != .normal { - Text(hoverText) + hoverIcon .bold() .textCase(.uppercase) .foregroundColor(secondaryTextColor) @@ -39,17 +39,23 @@ struct CopyableView: View { .multilineTextAlignment(.leading) .font(.system(.body, design: .monospaced)) } - .background(backgroundColor) + ._background(interactionState: interactionState) .frame(minWidth: 150, maxWidth: .infinity) - .cornerRadius(10) + } + + var body: some View { + content .onHover { hovering in withAnimation { interactionState = hovering ? .hovering : .normal } } - .onDrag { + .onDrag({ NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier) - } + }, preview: { + content + ._background(interactionState: .dragging) + }) .onTapGesture { copy() withAnimation { @@ -66,31 +72,20 @@ struct CopyableView: View { ) } - var hoverText: LocalizedStringKey { + var hoverIcon: Image { switch interactionState { - case .hovering: - return "copyable_click_to_copy_button" + case .hovering, .dragging: + return Image(systemName: "document.on.document") case .clicking: - return "copyable_copied" + return Image(systemName: "checkmark.circle.fill") case .normal: fatalError() } } - var backgroundColor: Color { - switch interactionState { - case .normal: - return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885) - case .hovering: - return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82) - case .clicking: - return .accentColor - } - } - var primaryTextColor: Color { switch interactionState { - case .normal, .hovering: + case .normal, .hovering, .dragging: return Color(.textColor) case .clicking: return .white @@ -99,7 +94,7 @@ struct CopyableView: View { var secondaryTextColor: Color { switch interactionState { - case .normal, .hovering: + case .normal, .hovering, .dragging: return Color(.secondaryLabelColor) case .clicking: return .white @@ -111,10 +106,57 @@ struct CopyableView: View { NSPasteboard.general.setString(text, forType: .string) } - private enum InteractionState { - case normal, hovering, clicking +} + +fileprivate enum InteractionState { + case normal, hovering, clicking, dragging +} + +extension View { + + fileprivate func _background(interactionState: InteractionState) -> some View { + modifier(BackgroundViewModifier(interactionState: interactionState)) + } + +} + +fileprivate struct BackgroundViewModifier: ViewModifier { + + @Environment(\.colorScheme) private var colorScheme + + let interactionState: InteractionState + + func body(content: Content) -> some View { + if interactionState == .dragging { + content + .background(backgroundColor(interactionState: interactionState), in: RoundedRectangle(cornerRadius: 15)) + } else { + if #available(macOS 26.0, *) { + content + // Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow. + .background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15)) + .glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15)) + + } else { + content + .background(backgroundColor(interactionState: interactionState)) + .cornerRadius(10) + } + } + } + + func backgroundColor(interactionState: InteractionState) -> Color { + switch interactionState { + case .normal: + return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885) + case .hovering, .dragging: + return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82) + case .clicking: + return .accentColor + } } + } #if DEBUG