initial commit
This commit is contained in:
36
Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift
Normal file
36
Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// CategoryCardView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct CategoryCardView: View {
|
||||
@State var category: Category
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Image("CookBook")
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.overlay(
|
||||
VStack {
|
||||
Spacer()
|
||||
Color.clear
|
||||
.background(
|
||||
.ultraThickMaterial
|
||||
)
|
||||
.overlay(
|
||||
Text(category.name)
|
||||
.font(.headline)
|
||||
)
|
||||
.frame(maxHeight: 30)
|
||||
}
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift
Normal file
37
Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// CategoryDetailView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
struct RecipeBookView: View {
|
||||
@State var categoryName: String
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
if let recipes = viewModel.recipes[categoryName] {
|
||||
ForEach(recipes, id: \.recipe_id) { recipe in
|
||||
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
|
||||
RecipeCardView(viewModel: viewModel, recipe: recipe)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(categoryName)
|
||||
.task {
|
||||
await viewModel.loadRecipeList(categoryName: categoryName)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.loadRecipeList(categoryName: categoryName, needsUpdate: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Nextcloud Cookbook iOS Client/Views/MainView.swift
Normal file
58
Nextcloud Cookbook iOS Client/Views/MainView.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 06.09.23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MainView: View {
|
||||
@StateObject var viewModel = MainViewModel()
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 0) {
|
||||
ForEach(viewModel.categories, id: \.name) { category in
|
||||
NavigationLink(
|
||||
destination: RecipeBookView(
|
||||
categoryName: category.name,
|
||||
viewModel: viewModel)
|
||||
) {
|
||||
CategoryCardView(category: category)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationTitle("CookBook")
|
||||
.toolbar {
|
||||
NavigationLink( destination: SettingsView()) {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadCategoryList()
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.loadCategoryList(needsUpdate: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
159
Nextcloud Cookbook iOS Client/Views/OnboardingView.swift
Normal file
159
Nextcloud Cookbook iOS Client/Views/OnboardingView.swift
Normal file
@@ -0,0 +1,159 @@
|
||||
//
|
||||
// OnboardingView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct OnboardingView: View {
|
||||
@ObservedObject var userSettings: UserSettings
|
||||
@State var selectedTab: Int = 0
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
WelcomeTab().tag(0)
|
||||
LoginTab(userSettings: userSettings).tag(1)
|
||||
}
|
||||
.tabViewStyle(.page)
|
||||
.background(
|
||||
selectedTab == 1 ? Color("ncblue").ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea()
|
||||
)
|
||||
.animation(.easeInOut, value: selectedTab)
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeTab: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Spacer()
|
||||
Image("CookBook")
|
||||
.resizable()
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
Text("Tank you for downloading")
|
||||
.font(.headline)
|
||||
Text("Nextcloud")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Cookbook")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Spacer()
|
||||
Text("This application is an open source effort and still in development. If you encounter any problems, please report them on our GitHub page.\n\nCurrently, only app token login is supported. You can create an app token in the nextcloud security settings.")
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.fontDesign(.rounded)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginTab: View {
|
||||
@ObservedObject var userSettings: UserSettings
|
||||
|
||||
enum Field {
|
||||
case server
|
||||
case username
|
||||
case token
|
||||
}
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image("nc-logo-white")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 150)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
LoginLabel(text: "Server address")
|
||||
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
|
||||
.focused($focusedField, equals: .server)
|
||||
.textContentType(.URL)
|
||||
.submitLabel(.next)
|
||||
.padding(.bottom)
|
||||
|
||||
LoginLabel(text: "User name")
|
||||
LoginTextField(example: "username", text: $userSettings.username)
|
||||
.focused($focusedField, equals: .username)
|
||||
.textContentType(.username)
|
||||
.submitLabel(.next)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
LoginLabel(text: "App Token")
|
||||
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token)
|
||||
.focused($focusedField, equals: .token)
|
||||
.textContentType(.password)
|
||||
.submitLabel(.join)
|
||||
HStack{
|
||||
Spacer()
|
||||
Button {
|
||||
userSettings.onboarding = false
|
||||
} label: {
|
||||
Text("Submit")
|
||||
.foregroundColor(.white)
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.white, lineWidth: 2)
|
||||
.foregroundColor(.clear)
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.onSubmit {
|
||||
switch focusedField {
|
||||
case .server:
|
||||
focusedField = .username
|
||||
case .username:
|
||||
focusedField = .token
|
||||
default:
|
||||
print("Attempting to log in ...")
|
||||
}
|
||||
}
|
||||
.fontDesign(.rounded)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginLabel: View {
|
||||
let text: String
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.foregroundColor(.white)
|
||||
.font(.headline)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginTextField: View {
|
||||
var example: String
|
||||
@Binding var text: String
|
||||
|
||||
var body: some View {
|
||||
TextField(example, text: $text)
|
||||
.textFieldStyle(.plain)
|
||||
.textCase(.lowercase)
|
||||
.foregroundColor(.white)
|
||||
.accentColor(.white)
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.white, lineWidth: 2)
|
||||
.foregroundColor(.clear)
|
||||
)
|
||||
}
|
||||
}
|
||||
35
Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift
Normal file
35
Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// RecipeCardView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct RecipeCardView: View {
|
||||
@State var viewModel: MainViewModel
|
||||
@State var recipe: Recipe
|
||||
@State var recipeThumb: UIImage?
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(uiImage: recipeThumb ?? UIImage(named: "CookBook")!)
|
||||
.resizable()
|
||||
.frame(maxWidth: 80, maxHeight: 80)
|
||||
Text(recipe.name)
|
||||
.font(.headline)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(.ultraThickMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.padding(.horizontal)
|
||||
.task {
|
||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false)
|
||||
}
|
||||
.refreshable {
|
||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false, needsUpdate: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
201
Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift
Normal file
201
Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift
Normal file
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// RecipeDetailView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct RecipeDetailView: View {
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
@State var recipe: Recipe
|
||||
@State var recipeDetail: RecipeDetail?
|
||||
@State var recipeImage: UIImage?
|
||||
@State var showTitle: Bool = false
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading) {
|
||||
if let recipeImage = recipeImage {
|
||||
Image(uiImage: recipeImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(height: 300)
|
||||
.clipped()
|
||||
} else {
|
||||
Color.blue
|
||||
.frame(height: 300)
|
||||
}
|
||||
|
||||
if let recipeDetail = recipeDetail {
|
||||
LazyVStack (alignment: .leading) {
|
||||
Divider()
|
||||
Text(recipeDetail.name)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding()
|
||||
.onDisappear {
|
||||
showTitle = true
|
||||
}
|
||||
.onAppear {
|
||||
showTitle = false
|
||||
}
|
||||
Divider()
|
||||
RecipeYieldSection(recipeDetail: recipeDetail)
|
||||
RecipeDurationSection(recipeDetail: recipeDetail)
|
||||
if(!recipeDetail.recipeIngredient.isEmpty) {
|
||||
RecipeIngredientSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
if(!recipeDetail.tool.isEmpty) {
|
||||
RecipeToolSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
if(!recipeDetail.recipeInstructions.isEmpty) {
|
||||
RecipeInstructionSection(recipeDetail: recipeDetail)
|
||||
}
|
||||
}.padding(.horizontal, 5)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(showTitle ? recipe.name : "")
|
||||
.task {
|
||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true)
|
||||
}
|
||||
.refreshable {
|
||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true, needsUpdate: true)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipeYieldSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Servings: \(recipeDetail.recipeYield)")
|
||||
Spacer()
|
||||
}.padding()
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipeDurationSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let prepTime = recipeDetail.prepTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Prep time")
|
||||
Text(formatDate(duration: prepTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
if let cookTime = recipeDetail.cookTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Cook time")
|
||||
Text(formatDate(duration: cookTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
if let totalTime = recipeDetail.totalTime {
|
||||
VStack {
|
||||
SecondaryLabel(text: "Total time")
|
||||
Text(formatDate(duration: totalTime))
|
||||
.lineLimit(1)
|
||||
}.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RecipeIngredientSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Ingredients")
|
||||
Spacer()
|
||||
}
|
||||
ForEach(recipeDetail.recipeIngredient, id: \.self) { ingredient in
|
||||
Text("\u{2022} \(ingredient)")
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(4)
|
||||
}
|
||||
}.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipeToolSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Tools")
|
||||
Spacer()
|
||||
}
|
||||
ForEach(recipeDetail.tool, id: \.self) { tool in
|
||||
Text("\u{2022} \(tool)")
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(4)
|
||||
}
|
||||
}.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipeInstructionSection: View {
|
||||
@State var recipeDetail: RecipeDetail
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Instructions")
|
||||
Spacer()
|
||||
}
|
||||
ForEach(0..<recipeDetail.recipeInstructions.count) { ix in
|
||||
HStack(alignment: .top) {
|
||||
Text("\(ix+1).")
|
||||
Text("\(recipeDetail.recipeInstructions[ix])")
|
||||
}.padding(4)
|
||||
}
|
||||
}.padding()
|
||||
.background(Color("accent"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct SecondaryLabel: View {
|
||||
let text: String
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.headline)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
55
Nextcloud Cookbook iOS Client/Views/SettingsView.swift
Normal file
55
Nextcloud Cookbook iOS Client/Views/SettingsView.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 15.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@StateObject var userSettings = UserSettings()
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack {
|
||||
SettingsSection(headline: "Language", description: "Language settings coming soon.")
|
||||
SettingsSection(headline: "Accent Color", description: "The accent color setting will be released in a future update.")
|
||||
Button("Log out") {
|
||||
print("Log out.")
|
||||
userSettings.serverAddress = ""
|
||||
userSettings.username = ""
|
||||
userSettings.token = ""
|
||||
userSettings.onboarding = true
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.accentColor(.red)
|
||||
.padding()
|
||||
|
||||
Button("Clear Cache") {
|
||||
print("Clear cache.")
|
||||
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.accentColor(.red)
|
||||
.padding()
|
||||
}
|
||||
}.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SettingsSection: View {
|
||||
@State var headline: String
|
||||
@State var description: String
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(headline)
|
||||
.font(.headline)
|
||||
Text(description)
|
||||
|
||||
Divider()
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user