Compare commits
No commits in common. "main" and "ac3031dd0599762896a97080350fbc4c1b58cd7b" have entirely different histories.
main
...
ac3031dd05
@ -17,10 +17,6 @@
|
||||
248D14F029A230FE00AE4C0D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248D14EF29A230FE00AE4C0D /* Constants.swift */; };
|
||||
248D14F329A2435000AE4C0D /* UtilityViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248D14F129A2435000AE4C0D /* UtilityViews.swift */; };
|
||||
248D14F629A243F200AE4C0D /* UtilityExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248D14F529A243F200AE4C0D /* UtilityExtensions.swift */; };
|
||||
24D4D3A729A9E02F0064E566 /* PaletteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3A629A9E02F0064E566 /* PaletteManager.swift */; };
|
||||
24E5FE8929A7AB4000794732 /* PaletteStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E5FE8829A7AB4000794732 /* PaletteStore.swift */; };
|
||||
24E5FE8B29A9944600794732 /* PaletteChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E5FE8A29A9944600794732 /* PaletteChooser.swift */; };
|
||||
24E5FE8D29A9B25800794732 /* PaletteEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E5FE8C29A9B25800794732 /* PaletteEditor.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -36,10 +32,6 @@
|
||||
248D14EF29A230FE00AE4C0D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
248D14F129A2435000AE4C0D /* UtilityViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityViews.swift; sourceTree = "<group>"; };
|
||||
248D14F529A243F200AE4C0D /* UtilityExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityExtensions.swift; sourceTree = "<group>"; };
|
||||
24D4D3A629A9E02F0064E566 /* PaletteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteManager.swift; sourceTree = "<group>"; };
|
||||
24E5FE8829A7AB4000794732 /* PaletteStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteStore.swift; sourceTree = "<group>"; };
|
||||
24E5FE8A29A9944600794732 /* PaletteChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteChooser.swift; sourceTree = "<group>"; };
|
||||
24E5FE8C29A9B25800794732 /* PaletteEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteEditor.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -72,15 +64,11 @@
|
||||
24145E7A29A1489700ECB9D1 /* EmojiArt */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24D4D3A629A9E02F0064E566 /* PaletteManager.swift */,
|
||||
248D14EF29A230FE00AE4C0D /* Constants.swift */,
|
||||
24145E7B29A1489700ECB9D1 /* EmojiArtApp.swift */,
|
||||
24145E7D29A1489700ECB9D1 /* EmojiArtDocumentView.swift */,
|
||||
248D14F129A2435000AE4C0D /* UtilityViews.swift */,
|
||||
24145E8A29A1498500ECB9D1 /* EmojiArtModel.swift */,
|
||||
24E5FE8829A7AB4000794732 /* PaletteStore.swift */,
|
||||
24E5FE8A29A9944600794732 /* PaletteChooser.swift */,
|
||||
24E5FE8C29A9B25800794732 /* PaletteEditor.swift */,
|
||||
248D14F529A243F200AE4C0D /* UtilityExtensions.swift */,
|
||||
248D14EB29A227C700AE4C0D /* EmojiArtDocument.swift */,
|
||||
24145E8C29A225E900ECB9D1 /* EmojiArtModel.Background.swift */,
|
||||
@ -173,14 +161,10 @@
|
||||
24145E7C29A1489700ECB9D1 /* EmojiArtApp.swift in Sources */,
|
||||
248D14EC29A227C700AE4C0D /* EmojiArtDocument.swift in Sources */,
|
||||
248D14F029A230FE00AE4C0D /* Constants.swift in Sources */,
|
||||
24E5FE8929A7AB4000794732 /* PaletteStore.swift in Sources */,
|
||||
24D4D3A729A9E02F0064E566 /* PaletteManager.swift in Sources */,
|
||||
24145E8B29A1498500ECB9D1 /* EmojiArtModel.swift in Sources */,
|
||||
24E5FE8D29A9B25800794732 /* PaletteEditor.swift in Sources */,
|
||||
24145E8D29A225E900ECB9D1 /* EmojiArtModel.Background.swift in Sources */,
|
||||
248D14F629A243F200AE4C0D /* UtilityExtensions.swift in Sources */,
|
||||
248D14F329A2435000AE4C0D /* UtilityViews.swift in Sources */,
|
||||
24E5FE8B29A9944600794732 /* PaletteChooser.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "E709882D-ACBF-4819-A5E7-499B9D69B8A9"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
@ -8,9 +8,8 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct const {
|
||||
static let defaultEmojiFontSize: CGFloat = 40
|
||||
static let coalescingInterval = 5.0
|
||||
static let paletteEditorMinWidth: CGFloat = 400
|
||||
static let paletteEditorMinHeight: CGFloat = 400
|
||||
}
|
||||
|
||||
@ -9,14 +9,10 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct EmojiArtApp: App {
|
||||
@StateObject var document = EmojiArtDocument()
|
||||
@StateObject var paletteStore = PaletteStore(named: "Default")
|
||||
|
||||
let document = EmojiArtDocument()
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
EmojiArtDocumentView(document: document)
|
||||
.environmentObject(paletteStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -66,10 +66,9 @@ class EmojiArtDocument: ObservableObject {
|
||||
@Published var backgroundImage: UIImage?
|
||||
@Published var backgroundImageFetchStatus = BackgroundImageFetchStatus.idle
|
||||
|
||||
enum BackgroundImageFetchStatus: Equatable {
|
||||
enum BackgroundImageFetchStatus {
|
||||
case idle
|
||||
case fetching
|
||||
case failed(URL )
|
||||
}
|
||||
|
||||
// MARK: - Intent(s)
|
||||
@ -110,9 +109,6 @@ class EmojiArtDocument: ObservableObject {
|
||||
if imageData != nil {
|
||||
self?.backgroundImage = UIImage(data: imageData!)
|
||||
}
|
||||
if self?.backgroundImage == nil {
|
||||
self?.backgroundImageFetchStatus = .failed(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,12 +8,14 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EmojiArtDocumentView: View {
|
||||
let testEmojis = "😀😃😄😁😆😅😂🤣🥲🥹☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩"
|
||||
|
||||
@ObservedObject var document: EmojiArtDocument
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
documentBody
|
||||
PaletteChooser(emojiFontSize: const.defaultEmojiFontSize)
|
||||
palette
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,31 +44,9 @@ struct EmojiArtDocumentView: View {
|
||||
drop(providers: providers, at: location, in: geometry)
|
||||
}
|
||||
.gesture(panGesture().simultaneously(with: zoomGesture()))
|
||||
.alert(item: $alertToShow) { alertToShow in
|
||||
alertToShow.alert()
|
||||
}
|
||||
.onChange(of: document.backgroundImageFetchStatus) { status in
|
||||
switch status {
|
||||
case .failed(let url):
|
||||
showBackgroundImageFetchFailedAlert(url)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var alertToShow: IdentifiableAlert?
|
||||
|
||||
private func showBackgroundImageFetchFailedAlert(_ url: URL) {
|
||||
alertToShow = IdentifiableAlert(id: "Fetch failed: " + url.absoluteString, alert: {
|
||||
Alert(
|
||||
title: Text("Background Image Fetch"),
|
||||
message: Text("Couldn't load image from \(url)."),
|
||||
dismissButton: .default(Text("OK")))
|
||||
})
|
||||
}
|
||||
|
||||
private func drop(providers: [NSItemProvider], at location: CGPoint, in geometry: GeometryProxy) -> Bool {
|
||||
var found = providers.loadObjects(ofType: URL.self) { url in
|
||||
document.setBackground(EmojiArtModel.Background.url(url.imageURL))
|
||||
@ -163,6 +143,25 @@ struct EmojiArtDocumentView: View {
|
||||
steadyStateZoomScale = min(hZoom, vZoom)
|
||||
}
|
||||
}
|
||||
|
||||
var palette: some View {
|
||||
SrcollingEmojiView(emojis: testEmojis)
|
||||
.font(.system(size: const.defaultEmojiFontSize))
|
||||
}
|
||||
}
|
||||
|
||||
struct SrcollingEmojiView: View {
|
||||
let emojis: String
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(emojis.map { String($0) }, id: \.self) { emoji in
|
||||
Text(emoji)
|
||||
.onDrag { NSItemProvider(object: emoji as NSString) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
//
|
||||
// PaletteChooser.swift
|
||||
// EmojiArt
|
||||
//
|
||||
// Created by ching on 2023/2/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PaletteChooser: View {
|
||||
var emojiFontSize: CGFloat = const.defaultEmojiFontSize
|
||||
var emojiFont: Font { .system(size: emojiFontSize) }
|
||||
|
||||
@EnvironmentObject var store: PaletteStore
|
||||
@State private var chosenPaletteIndex = 0
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
paletteButton
|
||||
body(for: store.palette(at: chosenPaletteIndex)
|
||||
)
|
||||
}.clipped()
|
||||
}
|
||||
|
||||
var paletteButton: some View {
|
||||
Button {
|
||||
withAnimation {
|
||||
chosenPaletteIndex = (chosenPaletteIndex + 1) % store.palettes.count
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "paintpalette")
|
||||
}
|
||||
.font(emojiFont)
|
||||
.contextMenu { contextMenu }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var contextMenu: some View {
|
||||
AnimatedActionButton(title: "Edit", systemImage: "pencil") {
|
||||
editing = true
|
||||
}
|
||||
AnimatedActionButton(title: "New", systemImage: "plus") {
|
||||
store.insertPalette(named: "New", emojis: "", at: chosenPaletteIndex)
|
||||
editing = true
|
||||
}
|
||||
AnimatedActionButton(title: "Delete", systemImage: "minus.circle") {
|
||||
chosenPaletteIndex = store.removePalette(at: chosenPaletteIndex)
|
||||
}
|
||||
AnimatedActionButton(title: "Manager", systemImage: "slider.vertical.3") {
|
||||
managing = true
|
||||
}
|
||||
gotoMenu
|
||||
}
|
||||
|
||||
var gotoMenu: some View {
|
||||
Menu {
|
||||
ForEach(store.palettes) {
|
||||
palette in AnimatedActionButton(title: palette.name) {
|
||||
if let index = store.palettes.index(matching: palette) {
|
||||
chosenPaletteIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Go To", systemImage: "text.insert")
|
||||
}
|
||||
}
|
||||
|
||||
func body(for palette: Palette) -> some View {
|
||||
HStack {
|
||||
Text(palette.name)
|
||||
SrcollingEmojiView(emojis: palette.emojis)
|
||||
.font(emojiFont)
|
||||
}
|
||||
.id(palette.id)
|
||||
.transition(rollTransaction)
|
||||
.popover(isPresented: $editing) {
|
||||
PaletteEditor(palette: $store.palettes[chosenPaletteIndex])
|
||||
}
|
||||
.sheet(isPresented: $managing) {
|
||||
PaletteManager()
|
||||
}
|
||||
}
|
||||
|
||||
@State private var editing = false
|
||||
@State private var managing = false
|
||||
|
||||
var rollTransaction: AnyTransition {
|
||||
AnyTransition.asymmetric(insertion: .offset(x: 0, y: emojiFontSize), removal: .offset(x: 0, y: -emojiFontSize))
|
||||
}
|
||||
}
|
||||
|
||||
struct SrcollingEmojiView: View {
|
||||
let emojis: String
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(emojis.map { String($0) }, id: \.self) { emoji in
|
||||
Text(emoji)
|
||||
.onDrag { NSItemProvider(object: emoji as NSString) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteChooser_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PaletteChooser()
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
//
|
||||
// PaletteEditor.swift
|
||||
// EmojiArt
|
||||
//
|
||||
// Created by ching on 2023/2/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PaletteEditor: View {
|
||||
@Binding var palette: Palette
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
nameSection
|
||||
addEmojiSection
|
||||
removeEmojiSection
|
||||
}
|
||||
.frame(minWidth: const.paletteEditorMinWidth, minHeight: const.paletteEditorMinHeight)
|
||||
.navigationTitle("Edit \(palette.name)")
|
||||
}
|
||||
|
||||
var nameSection: some View {
|
||||
Section(header: Text("Name")) {
|
||||
TextField("Name", text: $palette.name)
|
||||
}
|
||||
}
|
||||
|
||||
@State private var emojiToAdd = ""
|
||||
|
||||
var addEmojiSection: some View {
|
||||
Section(header: Text("Add Emoji")) {
|
||||
TextField("", text: $emojiToAdd)
|
||||
.onChange(of: emojiToAdd) { emojis in
|
||||
addEmojis(emojis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addEmojis(_ emojis: String) {
|
||||
palette.emojis = (emojis + palette.emojis)
|
||||
.filter { $0.isEmoji }
|
||||
}
|
||||
|
||||
var removeEmojiSection: some View {
|
||||
Section(header: Text("Remove Emoji")) {
|
||||
let emojis = palette.emojis.map { String($0) }
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: const.defaultEmojiFontSize))]) {
|
||||
ForEach(emojis, id: \.self) { emoji in
|
||||
Text(emoji)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
palette.emojis.removeAll(where: { String($0) == emoji })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteEditor_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PaletteEditor(palette: .constant(PaletteStore(named: "Priview").palette(at: 1)))
|
||||
.previewLayout(.fixed(width: 350, height: 350))
|
||||
PaletteEditor(palette: .constant(PaletteStore(named: "Priview").palette(at: 1)))
|
||||
.previewLayout(.fixed(width: 350, height: 650))
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
//
|
||||
// PaletteManager.swift
|
||||
// EmojiArt
|
||||
//
|
||||
// Created by ching on 2023/2/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PaletteManager: View {
|
||||
@EnvironmentObject var store: PaletteStore
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(store.palettes) { palette in
|
||||
NavigationLink(destination: PaletteEditor(palette: $store.palettes[palette])) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(palette.name)
|
||||
Text(palette.emojis)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
store.palettes.remove(atOffsets: indexSet)
|
||||
}
|
||||
.onMove { indexSet, newOffset in
|
||||
store.palettes.move(fromOffsets: indexSet, toOffset: newOffset)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Manage Palettes")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteManager_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PaletteManager()
|
||||
.previewDevice("iPhone 14 Pro Max")
|
||||
.environmentObject(PaletteStore(named: "preview"))
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
//
|
||||
// PaletteStore.swift
|
||||
// EmojiArt
|
||||
//
|
||||
// Created by ching on 2023/2/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Palette: Identifiable, Codable, Hashable {
|
||||
var name: String
|
||||
var emojis: String
|
||||
var id: Int
|
||||
|
||||
fileprivate init(name: String, emojis: String, id: Int) {
|
||||
self.name = name
|
||||
self.emojis = emojis
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
class PaletteStore: ObservableObject {
|
||||
let name: String
|
||||
|
||||
@Published var palettes = [Palette]() {
|
||||
didSet {
|
||||
storeInUserDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
private var userDefaultsKey: String {
|
||||
"PaletteStore:" + name
|
||||
}
|
||||
|
||||
private func storeInUserDefaults() {
|
||||
UserDefaults.standard.set(try? JSONEncoder().encode(palettes), forKey: userDefaultsKey)
|
||||
}
|
||||
|
||||
private func restoreFromUserDefaults() {
|
||||
if let jsonData = UserDefaults.standard.data(forKey: userDefaultsKey),
|
||||
let decodedPalettes = try? JSONDecoder().decode(Array<Palette>.self, from: jsonData) {
|
||||
palettes = decodedPalettes
|
||||
}
|
||||
}
|
||||
|
||||
init(named name: String) {
|
||||
self.name = name
|
||||
restoreFromUserDefaults()
|
||||
if palettes.isEmpty {
|
||||
print("using built-in palettes")
|
||||
insertPalette(named: "Vehicles", emojis: "🚗🚕🚙🚌🚎🏎🚓🚑🚒🚐🛻🚚🚛🚜🦯🦽🦼🛴🚲🛵🏍🛺🚨🚔🚍🚘🚖🛞🚡🚠🚟🚃🚋🚞🚝🚄🚅🚈🚂🚆🚇🚊🚉✈️🛫🛬🛩")
|
||||
insertPalette(named: "Sports", emojis: "⚽️🏀🏈⚾️🥎🎾🏐🏉🥏🎱🪀🏓🏸🏒🏑🥍🏏")
|
||||
} else {
|
||||
print("loaded palettes")
|
||||
}
|
||||
}
|
||||
|
||||
// Intent
|
||||
|
||||
func palette(at index: Int) -> Palette {
|
||||
let safeIndex = min(max(index, 0), palettes.count - 1)
|
||||
return palettes[safeIndex]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removePalette(at index: Int) -> Int {
|
||||
if palettes.count > 1, palettes.indices.contains(index) {
|
||||
palettes.remove(at: index)
|
||||
}
|
||||
return index % palettes.count
|
||||
}
|
||||
|
||||
func insertPalette(named name: String, emojis: String? = nil, at index: Int = 0) {
|
||||
let unique = (palettes.max(by: { $0.id < $1.id })?.id ?? 0) + 1
|
||||
let palette = Palette(name: name, emojis: emojis ?? "", id: unique)
|
||||
let safeIndex = min(max(index, 0), palettes.count)
|
||||
palettes.insert(palette, at: safeIndex)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user