|
|
@@ -11,20 +11,219 @@ struct PhotosDesktopWidgetView: View {
|
|
|
app.webURL ?? URL(string: "https://photos.google.com/")!
|
|
|
}
|
|
|
|
|
|
+ /// Google Photos–style dark shell (deep blue-grey).
|
|
|
+ private var shellGradient: LinearGradient {
|
|
|
+ LinearGradient(
|
|
|
+ colors: [
|
|
|
+ Color(red: 0.07, green: 0.10, blue: 0.14),
|
|
|
+ Color(red: 0.05, green: 0.07, blue: 0.11),
|
|
|
+ ],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
var body: some View {
|
|
|
Group {
|
|
|
if isPreview {
|
|
|
photosPreviewChrome
|
|
|
} else {
|
|
|
- VStack(spacing: 0) {
|
|
|
- galleryHeader(title: "Recent", subtitle: "photos.google.com")
|
|
|
- GooglePhotosEmbeddedWebView(startURL: baseURL)
|
|
|
- .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
- mediumShortcutRow
|
|
|
+ ZStack {
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .fill(shellGradient)
|
|
|
+
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ galleryChromeHeader
|
|
|
+ headerDivider
|
|
|
+ GooglePhotosEmbeddedWebView(startURL: baseURL)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ bottomChromeBar
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .strokeBorder(Color.white.opacity(0.10), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private var headerDivider: some View {
|
|
|
+ Rectangle()
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.white.opacity(0.14), Color.white.opacity(0.05)],
|
|
|
+ startPoint: .leading,
|
|
|
+ endPoint: .trailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .frame(height: 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var galleryChromeHeader: some View {
|
|
|
+ HStack(alignment: .center, spacing: 12) {
|
|
|
+ ZStack {
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.white.opacity(0.14), Color.white.opacity(0.06)],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .frame(width: 40, height: 40)
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.12), lineWidth: 1)
|
|
|
+ )
|
|
|
+ AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ }
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 3) {
|
|
|
+ Text(app.name)
|
|
|
+ .font(.system(size: 15, weight: .bold, design: .rounded))
|
|
|
+ .foregroundStyle(.white.opacity(0.96))
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Text("Recent")
|
|
|
+ .font(.system(size: 11, weight: .semibold, design: .rounded))
|
|
|
+ .foregroundStyle(Color(red: 0.55, green: 0.82, blue: 0.95).opacity(0.95))
|
|
|
+ Text("·")
|
|
|
+ .foregroundStyle(.white.opacity(0.35))
|
|
|
+ Text("Library")
|
|
|
+ .font(.system(size: 11, weight: .medium, design: .rounded))
|
|
|
+ .foregroundStyle(.white.opacity(0.55))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Spacer(minLength: 8)
|
|
|
+
|
|
|
+ Button(action: openPhotosHome) {
|
|
|
+ HStack(spacing: 5) {
|
|
|
+ Text("Open")
|
|
|
+ .font(.system(size: 12, weight: .bold, design: .rounded))
|
|
|
+ Image(systemName: "arrow.up.right.circle.fill")
|
|
|
+ .font(.system(size: 14, weight: .semibold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(.white.opacity(0.95))
|
|
|
+ .padding(.horizontal, 11)
|
|
|
+ .padding(.vertical, 7)
|
|
|
+ .background(
|
|
|
+ Capsule(style: .continuous)
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.white.opacity(0.22), Color.white.opacity(0.10)],
|
|
|
+ startPoint: .top,
|
|
|
+ endPoint: .bottom
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ Capsule(style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.18), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 14)
|
|
|
+ .padding(.vertical, 12)
|
|
|
+ .background(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.black.opacity(0.42), Color.black.opacity(0.22)],
|
|
|
+ startPoint: .top,
|
|
|
+ endPoint: .bottom
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Subtle multi-hue strip echoing Google Photos branding.
|
|
|
+ private var photosAccentStrip: some View {
|
|
|
+ LinearGradient(
|
|
|
+ colors: [
|
|
|
+ Color(red: 0.26, green: 0.52, blue: 0.96),
|
|
|
+ Color(red: 0.18, green: 0.72, blue: 0.45),
|
|
|
+ Color(red: 0.98, green: 0.75, blue: 0.18),
|
|
|
+ Color(red: 0.92, green: 0.35, blue: 0.25),
|
|
|
+ ],
|
|
|
+ startPoint: .leading,
|
|
|
+ endPoint: .trailing
|
|
|
+ )
|
|
|
+ .frame(height: 2)
|
|
|
+ .opacity(0.85)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var bottomChromeBar: some View {
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ photosAccentStrip
|
|
|
+ HStack(spacing: 10) {
|
|
|
+ PhotosShortcutChip(title: "Albums", systemImage: "rectangle.stack.fill") {
|
|
|
+ openPhotosPath("/albums")
|
|
|
+ }
|
|
|
+ PhotosShortcutChip(title: "Memories", systemImage: "sparkles") {
|
|
|
+ openPhotosPath("/memories")
|
|
|
+ }
|
|
|
+ PhotosShortcutChip(title: "Search", systemImage: "magnifyingglass") {
|
|
|
+ openPhotosPath("/search")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 12)
|
|
|
+ .padding(.vertical, 12)
|
|
|
+ .background(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.black.opacity(0.55), Color.black.opacity(0.38)],
|
|
|
+ startPoint: .top,
|
|
|
+ endPoint: .bottom
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private struct PhotosShortcutChip: View {
|
|
|
+ let title: String
|
|
|
+ let systemImage: String
|
|
|
+ let action: () -> Void
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: action) {
|
|
|
+ VStack(spacing: 6) {
|
|
|
+ Image(systemName: systemImage)
|
|
|
+ .font(.system(size: 15, weight: .semibold))
|
|
|
+ .foregroundStyle(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [
|
|
|
+ Color.white.opacity(0.95),
|
|
|
+ Color.white.opacity(0.72),
|
|
|
+ ],
|
|
|
+ startPoint: .top,
|
|
|
+ endPoint: .bottom
|
|
|
+ )
|
|
|
+ )
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 10, weight: .bold, design: .rounded))
|
|
|
+ .foregroundStyle(.white.opacity(0.88))
|
|
|
+ .lineLimit(1)
|
|
|
+ .minimumScaleFactor(0.85)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 11)
|
|
|
+ .padding(.horizontal, 6)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.white.opacity(0.14), Color.white.opacity(0.05)],
|
|
|
+ startPoint: .top,
|
|
|
+ endPoint: .bottom
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.14), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private var photosPreviewChrome: some View {
|
|
|
@@ -32,72 +231,49 @@ struct PhotosDesktopWidgetView: View {
|
|
|
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
.fill(
|
|
|
LinearGradient(
|
|
|
- colors: [Color(red: 0.12, green: 0.16, blue: 0.22), Color(red: 0.05, green: 0.08, blue: 0.12)],
|
|
|
+ colors: [
|
|
|
+ Color(red: 0.10, green: 0.14, blue: 0.20),
|
|
|
+ Color(red: 0.04, green: 0.06, blue: 0.10),
|
|
|
+ ],
|
|
|
startPoint: .topLeading,
|
|
|
endPoint: .bottomTrailing
|
|
|
)
|
|
|
)
|
|
|
- VStack(spacing: 12) {
|
|
|
- AppIconView(app: app, size: 48, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ VStack(spacing: 14) {
|
|
|
+ ZStack {
|
|
|
+ Circle()
|
|
|
+ .fill(
|
|
|
+ LinearGradient(
|
|
|
+ colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
|
|
|
+ startPoint: .topLeading,
|
|
|
+ endPoint: .bottomTrailing
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .frame(width: 72, height: 72)
|
|
|
+ .overlay(Circle().stroke(Color.white.opacity(0.14), lineWidth: 1))
|
|
|
+ AppIconView(app: app, size: 44, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
+ }
|
|
|
Text("Recent gallery")
|
|
|
- .font(.system(size: 15, weight: .bold))
|
|
|
- .foregroundStyle(.white.opacity(0.92))
|
|
|
- Text("Live Google Photos grid in the panel after you sign in.")
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(.white.opacity(0.65))
|
|
|
+ .font(.system(size: 16, weight: .bold, design: .rounded))
|
|
|
+ .foregroundStyle(.white.opacity(0.94))
|
|
|
+ Text("Your library appears here after you sign in to Google Photos in the panel.")
|
|
|
+ .font(.system(size: 11, weight: .medium, design: .rounded))
|
|
|
+ .foregroundStyle(.white.opacity(0.62))
|
|
|
.multilineTextAlignment(.center)
|
|
|
- .padding(.horizontal, 12)
|
|
|
- }
|
|
|
- .padding(16)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private func galleryHeader(title: String, subtitle: String) -> some View {
|
|
|
- HStack(spacing: 10) {
|
|
|
- AppIconView(app: app, size: 32, showAppBackground: false, iconPaddingFactor: 0.08)
|
|
|
- VStack(alignment: .leading, spacing: 2) {
|
|
|
- Text(app.name)
|
|
|
- .font(.system(size: 14, weight: .bold))
|
|
|
- .foregroundStyle(.white.opacity(0.95))
|
|
|
- Text(title)
|
|
|
- .font(.system(size: 11, weight: .semibold))
|
|
|
- .foregroundStyle(.white.opacity(0.65))
|
|
|
+ .lineSpacing(2)
|
|
|
+ .padding(.horizontal, 18)
|
|
|
}
|
|
|
- Spacer(minLength: 0)
|
|
|
- Text(subtitle)
|
|
|
- .font(.system(size: 9, weight: .medium))
|
|
|
- .foregroundStyle(.white.opacity(0.4))
|
|
|
+ .padding(20)
|
|
|
}
|
|
|
- .padding(.horizontal, 14)
|
|
|
- .padding(.vertical, 10)
|
|
|
- .background(Color.black.opacity(0.35))
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
|
+ .strokeBorder(Color.white.opacity(0.10), lineWidth: 1)
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- private var mediumShortcutRow: some View {
|
|
|
- HStack(spacing: 8) {
|
|
|
- shortcutPill(title: "Albums", systemImage: "rectangle.stack", path: "/albums")
|
|
|
- shortcutPill(title: "Memories", systemImage: "sparkles", path: "/memories")
|
|
|
- shortcutPill(title: "Search", systemImage: "magnifyingglass", path: "/search")
|
|
|
- }
|
|
|
- .padding(.horizontal, 12)
|
|
|
- .padding(.vertical, 10)
|
|
|
- .background(Color.black.opacity(0.4))
|
|
|
- }
|
|
|
-
|
|
|
- private func shortcutPill(title: String, systemImage: String, path: String) -> some View {
|
|
|
- Button(action: { openPhotosPath(path) }) {
|
|
|
- HStack(spacing: 6) {
|
|
|
- Image(systemName: systemImage)
|
|
|
- .font(.system(size: 11, weight: .semibold))
|
|
|
- Text(title)
|
|
|
- .font(.system(size: 11, weight: .bold))
|
|
|
- }
|
|
|
- .foregroundStyle(.white.opacity(0.92))
|
|
|
- .padding(.horizontal, 12)
|
|
|
- .padding(.vertical, 8)
|
|
|
- .background(Capsule().fill(Color.white.opacity(0.12)))
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
+ private func openPhotosHome() {
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: baseURL, title: app.name)
|
|
|
}
|
|
|
|
|
|
private func openPhotosPath(_ path: String) {
|