mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-25 10:08:34 +02:00
feat: add dark mode support for ipad
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
||||||
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
||||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
||||||
|
EC86C5822C4010CC00C07D21 /* PhotoDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC86C5812C4010CC00C07D21 /* PhotoDock.swift */; };
|
||||||
EC8C9DCE2C39882500A8F3C4 /* NSSyncScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */; };
|
EC8C9DCE2C39882500A8F3C4 /* NSSyncScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */; };
|
||||||
EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */; };
|
EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */; };
|
||||||
EC8F54AE2C2AF5A4001C7C74 /* LineGridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */; };
|
EC8F54AE2C2AF5A4001C7C74 /* LineGridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */; };
|
||||||
@@ -164,6 +165,7 @@
|
|||||||
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
|
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
|
||||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
EC86C5812C4010CC00C07D21 /* PhotoDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoDock.swift; sourceTree = "<group>"; };
|
||||||
EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSyncScrollView.swift; sourceTree = "<group>"; };
|
EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSyncScrollView.swift; sourceTree = "<group>"; };
|
||||||
EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridMode.swift; sourceTree = "<group>"; };
|
EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridMode.swift; sourceTree = "<group>"; };
|
||||||
EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineGridVertex.swift; sourceTree = "<group>"; };
|
EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineGridVertex.swift; sourceTree = "<group>"; };
|
||||||
@@ -486,6 +488,14 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EC86C5802C4010BE00C07D21 /* PhotoDock */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EC86C5812C4010CC00C07D21 /* PhotoDock.swift */,
|
||||||
|
);
|
||||||
|
path = PhotoDock;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EC8C9DCC2C3987FD00A8F3C4 /* AppKit */ = {
|
EC8C9DCC2C3987FD00A8F3C4 /* AppKit */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -533,6 +543,7 @@
|
|||||||
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
EC86C5802C4010BE00C07D21 /* PhotoDock */,
|
||||||
ECDAC0792C318DAF0000ED77 /* ElementToolbar */,
|
ECDAC0792C318DAF0000ED77 /* ElementToolbar */,
|
||||||
ECBE52942C1D58F5006BDB3D /* PhotoPreview */,
|
ECBE52942C1D58F5006BDB3D /* PhotoPreview */,
|
||||||
EC1B783B2BFA0AAC005A34E2 /* Toolbar */,
|
EC1B783B2BFA0AAC005A34E2 /* Toolbar */,
|
||||||
@@ -1029,6 +1040,7 @@
|
|||||||
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
|
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
|
||||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
||||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
||||||
|
EC86C5822C4010CC00C07D21 /* PhotoDock.swift in Sources */,
|
||||||
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
||||||
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */,
|
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */,
|
||||||
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
||||||
|
|||||||
@@ -10,17 +10,13 @@ import PhotosUI
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
struct ElementToolbar: View {
|
struct ElementToolbar: View {
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
|
||||||
let size: CGFloat = 40
|
let size: CGFloat = 40
|
||||||
@ObservedObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
@ObservedObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
@State var opensCamera: Bool = false
|
|
||||||
@State var isCameraAccessDenied: Bool = false
|
|
||||||
@State var photosPickerItem: PhotosPickerItem?
|
|
||||||
|
|
||||||
@Namespace var namespace
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -31,10 +27,7 @@ struct ElementToolbar: View {
|
|||||||
} else {
|
} else {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
if tool.selection == .photo {
|
if tool.selection == .photo {
|
||||||
photoOption
|
PhotoDock(tool: tool, canvas: canvas)
|
||||||
.padding(.bottom, 10)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
|
||||||
} else {
|
} else {
|
||||||
compactToolbar
|
compactToolbar
|
||||||
}
|
}
|
||||||
@@ -42,42 +35,7 @@ struct ElementToolbar: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
|
||||||
.fullScreenCover(isPresented: $opensCamera) {
|
|
||||||
let image: Binding<UIImage?> = Binding {
|
|
||||||
tool.selectedPhotoItem?.image
|
|
||||||
} set: { image in
|
|
||||||
guard let image else { return }
|
|
||||||
tool.selectPhoto(image, for: canvas.canvasID)
|
|
||||||
}
|
|
||||||
CameraView(image: image, canvas: canvas)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
}
|
|
||||||
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
|
|
||||||
Button {
|
|
||||||
if let url = URL(string: Platform.Application.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") {
|
|
||||||
Platform.Application.shared.open(url)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("Open Settings")
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) { }
|
|
||||||
} message: {
|
|
||||||
Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
.onChange(of: photosPickerItem) { oldValue, newValue in
|
|
||||||
if newValue != nil {
|
|
||||||
Task {
|
|
||||||
tool.isLoadingPhoto = true
|
|
||||||
let data = try? await newValue?.loadTransferable(type: Data.self)
|
|
||||||
if let data, let image = Platform.Image(data: data) {
|
|
||||||
tool.selectPhoto(image, for: canvas.canvasID)
|
|
||||||
}
|
|
||||||
photosPickerItem = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var regularToolbar: some View {
|
var regularToolbar: some View {
|
||||||
@@ -91,7 +49,7 @@ struct ElementToolbar: View {
|
|||||||
.fontWeight(.heavy)
|
.fontWeight(.heavy)
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor)
|
.foregroundStyle(tool.selection == .hand ? colorScheme == .light ? Color.white : Color.black : Color.accentColor)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.contentShape(.rect(cornerRadius: 8))
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
@@ -104,7 +62,6 @@ struct ElementToolbar: View {
|
|||||||
if tool.selection == .hand {
|
if tool.selection == .hand {
|
||||||
Color.accentColor
|
Color.accentColor
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
@@ -116,7 +73,7 @@ struct ElementToolbar: View {
|
|||||||
.fontWeight(.heavy)
|
.fontWeight(.heavy)
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor)
|
.foregroundStyle(tool.selection == .pen ? colorScheme == .light ? Color.white : Color.black : Color.accentColor)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.contentShape(.rect(cornerRadius: 8))
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
@@ -129,7 +86,6 @@ struct ElementToolbar: View {
|
|||||||
if tool.selection == .pen {
|
if tool.selection == .pen {
|
||||||
Color.accentColor
|
Color.accentColor
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
@@ -141,7 +97,7 @@ struct ElementToolbar: View {
|
|||||||
Image(systemName: "photo")
|
Image(systemName: "photo")
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor)
|
.foregroundStyle(tool.selection == .photo ? colorScheme == .light ? Color.white : Color.black : Color.accentColor)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.contentShape(.rect(cornerRadius: 8))
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
@@ -151,22 +107,10 @@ struct ElementToolbar: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
#endif
|
#endif
|
||||||
.background {
|
.background {
|
||||||
#if os(iOS)
|
|
||||||
if tool.selection == .photo {
|
if tool.selection == .photo {
|
||||||
Color.accentColor
|
Color.accentColor
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if tool.selection != .photo {
|
|
||||||
Color.clear
|
|
||||||
.matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tool.selection == .photo {
|
|
||||||
photoOption
|
|
||||||
.matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace)
|
|
||||||
.transition(.blurReplace.animation(.easeIn(duration: 0.1)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,75 +160,5 @@ struct ElementToolbar: View {
|
|||||||
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
|
|
||||||
var photoOption: some View {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
#if os(iOS)
|
|
||||||
Button {
|
|
||||||
openCamera()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "camera.fill")
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
|
||||||
.contentShape(.rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
.hoverEffect(.lift)
|
|
||||||
#endif
|
|
||||||
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
|
||||||
Image(systemName: "photo.fill.on.rectangle.fill")
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
|
||||||
.contentShape(.rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.hoverEffect(.lift)
|
|
||||||
#else
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
#endif
|
|
||||||
if horizontalSizeClass == .compact {
|
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.frame(height: size)
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
tool.selectTool(.hand)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
|
||||||
.contentShape(.rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
#if os(iOS)
|
|
||||||
.hoverEffect(.lift)
|
|
||||||
#else
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(.regularMaterial)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func openCamera() {
|
|
||||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
||||||
switch status {
|
|
||||||
case .notDetermined:
|
|
||||||
AVCaptureDevice.requestAccess(for: .video) { status in
|
|
||||||
withAnimation {
|
|
||||||
if status {
|
|
||||||
opensCamera = true
|
|
||||||
} else {
|
|
||||||
isCameraAccessDenied = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .authorized:
|
|
||||||
opensCamera = true
|
|
||||||
default:
|
|
||||||
isCameraAccessDenied = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,14 +66,17 @@ struct MemoView: View {
|
|||||||
var canvasView: some View {
|
var canvasView: some View {
|
||||||
CanvasView(tool: tool, canvas: canvas, history: history)
|
CanvasView(tool: tool, canvas: canvas, history: history)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.overlay(alignment: .bottomTrailing) {
|
.overlay(alignment: .trailing) {
|
||||||
switch tool.selection {
|
switch tool.selection {
|
||||||
case .pen:
|
case .pen:
|
||||||
PenDock(tool: tool, canvas: canvas)
|
PenDock(tool: tool, canvas: canvas)
|
||||||
case .photo:
|
case .photo:
|
||||||
if let photoItem = tool.selectedPhotoItem {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
PhotoPreview(photoItem: photoItem, tool: tool)
|
PhotoDock(tool: tool, canvas: canvas)
|
||||||
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
if let photoItem = tool.selectedPhotoItem {
|
||||||
|
PhotoPreview(photoItem: photoItem, tool: tool)
|
||||||
|
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ struct PenDock: View {
|
|||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.frame(width: width)
|
.frame(width: width)
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 3)
|
||||||
.background(alignment: .trailing) {
|
.background(alignment: .trailing) {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.fill(.regularMaterial)
|
.fill(.regularMaterial)
|
||||||
|
|||||||
198
Memola/Features/Memo/PhotoDock/PhotoDock.swift
Normal file
198
Memola/Features/Memo/PhotoDock/PhotoDock.swift
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
//
|
||||||
|
// PhotoDock.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 7/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
|
struct PhotoDock: View {
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
|
||||||
|
let size: CGFloat = 40
|
||||||
|
|
||||||
|
@ObservedObject var tool: Tool
|
||||||
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
|
@State var opensCamera: Bool = false
|
||||||
|
@State var isCameraAccessDenied: Bool = false
|
||||||
|
@State var photosPickerItem: PhotosPickerItem?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if horizontalSizeClass == .regular {
|
||||||
|
photoOption
|
||||||
|
} else {
|
||||||
|
compactPhotoOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.fullScreenCover(isPresented: $opensCamera) {
|
||||||
|
let image: Binding<UIImage?> = Binding {
|
||||||
|
tool.selectedPhotoItem?.image
|
||||||
|
} set: { image in
|
||||||
|
guard let image else { return }
|
||||||
|
tool.selectPhoto(image, for: canvas.canvasID)
|
||||||
|
}
|
||||||
|
CameraView(image: image, canvas: canvas)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
|
||||||
|
Button {
|
||||||
|
if let url = URL(string: Platform.Application.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") {
|
||||||
|
Platform.Application.shared.open(url)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Open Settings")
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
.onChange(of: photosPickerItem) { oldValue, newValue in
|
||||||
|
if newValue != nil {
|
||||||
|
Task {
|
||||||
|
tool.isLoadingPhoto = true
|
||||||
|
let data = try? await newValue?.loadTransferable(type: Data.self)
|
||||||
|
if let data, let image = Platform.Image(data: data) {
|
||||||
|
tool.selectPhoto(image, for: canvas.canvasID)
|
||||||
|
}
|
||||||
|
photosPickerItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var photoOption: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
#if os(iOS)
|
||||||
|
Button {
|
||||||
|
openCamera()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "camera.fill")
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
#endif
|
||||||
|
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||||
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
#else
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#endif
|
||||||
|
if horizontalSizeClass == .compact {
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.frame(height: size)
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selectTool(.hand)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
#else
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
||||||
|
}
|
||||||
|
|
||||||
|
var compactPhotoOption: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
#if os(iOS)
|
||||||
|
Button {
|
||||||
|
openCamera()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "camera.fill")
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
#endif
|
||||||
|
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||||
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
#else
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#endif
|
||||||
|
if horizontalSizeClass == .compact {
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.frame(height: size)
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selectTool(.hand)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.contentShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
#else
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||||
|
}
|
||||||
|
|
||||||
|
func openCamera() {
|
||||||
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
|
switch status {
|
||||||
|
case .notDetermined:
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { status in
|
||||||
|
withAnimation {
|
||||||
|
if status {
|
||||||
|
opensCamera = true
|
||||||
|
} else {
|
||||||
|
isCameraAccessDenied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .authorized:
|
||||||
|
opensCamera = true
|
||||||
|
default:
|
||||||
|
isCameraAccessDenied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,33 @@
|
|||||||
{
|
{
|
||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "extended-srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF5",
|
||||||
|
"green" : "0x7E",
|
||||||
|
"red" : "0x00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "extended-srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFF",
|
||||||
|
"green" : "0xCE",
|
||||||
|
"red" : "0x99"
|
||||||
|
}
|
||||||
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user