UI Overhaul

This commit is contained in:
stossy11 2024-10-09 01:25:14 +11:00
parent 85cf5048ff
commit 908044fd02
8 changed files with 484 additions and 36 deletions

View File

@ -0,0 +1,44 @@
import CoreMotion
import SwiftUI
class MotionManager: ObservableObject {
private var motionManager = CMMotionManager()
@Published var gyroData: CMGyroData?
@Published var accelerometerData: CMAccelerometerData?
init() {
startGyroUpdates()
startAccelerometerUpdates()
}
func startGyroUpdates() {
if motionManager.isGyroAvailable {
motionManager.gyroUpdateInterval = 1.0 / 60.0 // Update at 60 Hz
motionManager.startGyroUpdates(to: .main) { [weak self] data, error in
if let error = error {
print("Gyro Error: \(error.localizedDescription)")
return
}
self?.gyroData = data
}
}
}
func startAccelerometerUpdates() {
if motionManager.isAccelerometerAvailable {
motionManager.accelerometerUpdateInterval = 1.0 / 60.0 // Update at 60 Hz
motionManager.startAccelerometerUpdates(to: .main) { [weak self] data, error in
if let error = error {
print("Accelerometer Error: \(error.localizedDescription)")
return
}
self?.accelerometerData = data
}
}
}
deinit {
motionManager.stopGyroUpdates()
motionManager.stopAccelerometerUpdates()
}
}

View File

@ -0,0 +1,88 @@
//
// GameListView.swift
// Pomelo
//
// Created by Stossy11 on 9/10/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
struct GameListView: View {
@State var core: Core
@State private var searchText = ""
@State var game: Int = 1
@State var startgame: Bool = false
@State var showAlert = false
@State var selectedGame: PomeloGame?
@State var alertMessage: Alert? = nil
var body: some View {
let filteredGames = core.games.filter { game in
guard let PomeloGame = game as? PomeloGame else { return false }
return searchText.isEmpty || PomeloGame.title.localizedCaseInsensitiveContains(searchText)
}
ScrollView {
VStack {
VStack(alignment: .leading) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200))], spacing: 2) {
ForEach(0..<filteredGames.count, id: \.self) { index in
let game = filteredGames[index] // Use filteredGames here
NavigationLink(destination: SudachiEmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
GameIconView(game: game, selectedGame: $selectedGame)
.frame(maxWidth: 200, minHeight: 250)
}
.onAppear {
selectedGame = filteredGames.first
}
.contextMenu {
Button(action: {
do {
try LibraryManager.shared.removerom(filteredGames[index])
} catch {
showAlert = true
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription))
}
}) {
Text("Remove")
}
Button(action: {
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") {
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil)
}
}) {
if ProcessInfo.processInfo.isMacCatalystApp {
Text("Open in Finder")
} else {
Text("Open in Files")
}
}
NavigationLink(destination: SudachiEmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
Text("Launch")
}
}
}
}
}
.searchable(text: $searchText)
.padding()
}
.onAppear {
refreshcore()
}
.alert(isPresented: $showAlert) {
alertMessage ?? Alert(title: Text("Error Not Found"))
}
}
}
func refreshcore() {
do {
core = try LibraryManager.shared.library()
} catch {
print("Failed to fetch library: \(error)")
return
}
}
}

View File

@ -0,0 +1,67 @@
struct BottomMenuView: View {
@State var core: Core
var body: some View {
HStack(spacing: 40) {
Button {
if let url = URL(string: "messages://") { // Replace appScheme with the actual URL scheme of the app
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { (success) in
if success {
print("App opened successfully")
} else {
print("Failed to open app")
}
}
} else {
print("The app is not installed.")
}
}
} label: {
Circle()
.overlay {
Image(systemName: "message")
.font(.system(size: 30))
.foregroundColor(.red)
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .darkGray))
}
NavigationLink(destination: ScreenshotGridView(core: core)) {
Circle()
.overlay {
Image(systemName: "photo")
.font(.system(size: 30))
.foregroundColor(.blue)
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .darkGray))
}
// ScreenshotGridView
NavigationLink(destination: SettingsView(core: core)) {
Circle()
.overlay {
Image(systemName: "gearshape")
.foregroundColor(.white)
.font(.system(size: 30))
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .darkGray))
}
NavigationLink(destination: BootOSView()) {
Circle()
.overlay {
Image(systemName: "power")
.foregroundColor(.white)
.font(.system(size: 30))
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .darkGray))
}
}
.padding(.bottom, 20)
}
}

View File

@ -0,0 +1,76 @@
struct TopBarView: View {
@State private var currentDate: Date = Date()
@State private var batteryLevel: Float = UIDevice.current.batteryLevel
@State private var batteryState: UIDevice.BatteryState = UIDevice.current.batteryState
private var timer: Publishers.Autoconnect<Timer.TimerPublisher> {
Timer.publish(every: 60, on: .main, in: .common).autoconnect() // Update every minute
}
var body: some View {
let hour = Calendar.current.component(.hour, from: currentDate)
let minutes = Calendar.current.component(.minute, from: currentDate)
HStack {
Image(systemName: "person.crop.circle.fill")
.resizable()
.frame(width: 40, height: 40)
Spacer()
Text("\(hour % 12 == 0 ? 12 : hour % 12):\(String(format: "%02d", minutes)) \(hour >= 12 ? "PM" : "AM")")
// .foregroundColor(.black)
.font(.system(size: 22))
Spacer()
HStack {
Image(systemName: "wifi")
Image(systemName: batteryImageName(for: batteryLevel))
}
}
.padding(.horizontal, 20)
.padding(.vertical, 10)
.onReceive(timer) { _ in
currentDate = Date()
}
.onAppear {
UIDevice.current.isBatteryMonitoringEnabled = true
batteryLevel = UIDevice.current.batteryLevel
batteryState = UIDevice.current.batteryState
// Add observers for battery level and state changes
NotificationCenter.default.addObserver(
forName: UIDevice.batteryLevelDidChangeNotification,
object: nil,
queue: .main) { _ in
self.batteryLevel = UIDevice.current.batteryLevel
}
NotificationCenter.default.addObserver(
forName: UIDevice.batteryStateDidChangeNotification,
object: nil,
queue: .main) { _ in
self.batteryState = UIDevice.current.batteryState
}
}
.onDisappear {
// Remove observers when the view disappears
NotificationCenter.default.removeObserver(self)
}
}
private func batteryImageName(for level: Float) -> String {
switch level {
case 0.0: return "battery.0"
case 0.1..<0.25: return "battery.25"
case 0.25..<0.5: return "battery.50"
case 0.5..<0.75: return "battery.75"
case 0.75..<1.0: return "battery.75"
default: return "battery.100"
}
}
}

View File

@ -1,36 +0,0 @@
//
// NavView.swift
// Pomelo
//
// Created by Stossy11 on 14/7/2024.
//
import SwiftUI
import Sudachi
struct NavView: View {
@Binding var core: Core
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
LibraryView(core: $core)
.tabItem {
Label("Library", systemImage: "rectangle.on.rectangle")
}
.tag(0)
BootOSView(core: $core, currentnavigarion: $selectedTab)
.toolbar(.hidden, for: .tabBar)
.tabItem {
Label("Boot OS", systemImage: "house")
}
.tag(1)
SettingsView(core: core)
.tabItem {
Label("Settings", systemImage: "gear")
}
.tag(2)
}
}
}

View File

@ -0,0 +1,209 @@
#if os(iOS)
import SwiftUI
struct ZoomableModifier: ViewModifier {
let minZoomScale: CGFloat
let doubleTapZoomScale: CGFloat
@State private var lastTransform: CGAffineTransform = .identity
@State private var transform: CGAffineTransform = .identity
@State private var contentSize: CGSize = .zero
func body(content: Content) -> some View {
content
.background(alignment: .topLeading) {
GeometryReader { proxy in
Color.clear
.onAppear {
contentSize = proxy.size
}
}
}
.animatableTransformEffect(transform)
.gesture(dragGesture, including: transform == .identity ? .none : .all)
.modify { view in
if #available(iOS 17.0, *) {
view.gesture(magnificationGesture)
} else {
view.gesture(oldMagnificationGesture)
}
}
.gesture(doubleTapGesture)
}
@available(iOS, introduced: 16.0, deprecated: 17.0)
private var oldMagnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
let zoomFactor = 0.5
let scale = value * zoomFactor
transform = lastTransform.scaledBy(x: scale, y: scale)
}
.onEnded { _ in
onEndGesture()
}
}
@available(iOS 17.0, *)
private var magnificationGesture: some Gesture {
MagnifyGesture(minimumScaleDelta: 0)
.onChanged { value in
let newTransform = CGAffineTransform.anchoredScale(
scale: value.magnification,
anchor: value.startAnchor.scaledBy(contentSize)
)
withAnimation(.interactiveSpring) {
transform = lastTransform.concatenating(newTransform)
}
}
.onEnded { _ in
onEndGesture()
}
}
private var doubleTapGesture: some Gesture {
SpatialTapGesture(count: 2)
.onEnded { value in
let newTransform: CGAffineTransform =
if transform.isIdentity {
.anchoredScale(scale: doubleTapZoomScale, anchor: value.location)
} else {
.identity
}
withAnimation(.linear(duration: 0.15)) {
transform = newTransform
lastTransform = newTransform
}
}
}
private var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
withAnimation(.interactiveSpring) {
transform = lastTransform.translatedBy(
x: value.translation.width / transform.scaleX,
y: value.translation.height / transform.scaleY
)
}
}
.onEnded { _ in
onEndGesture()
}
}
private func onEndGesture() {
let newTransform = limitTransform(transform)
withAnimation(.snappy(duration: 0.1)) {
transform = newTransform
lastTransform = newTransform
}
}
private func limitTransform(_ transform: CGAffineTransform) -> CGAffineTransform {
let scaleX = transform.scaleX
let scaleY = transform.scaleY
if scaleX < minZoomScale
|| scaleY < minZoomScale
{
return .identity
}
let maxX = contentSize.width * (scaleX - 1)
let maxY = contentSize.height * (scaleY - 1)
if transform.tx > 0
|| transform.tx < -maxX
|| transform.ty > 0
|| transform.ty < -maxY
{
let tx = min(max(transform.tx, -maxX), 0)
let ty = min(max(transform.ty, -maxY), 0)
var transform = transform
transform.tx = tx
transform.ty = ty
return transform
}
return transform
}
}
public extension View {
@ViewBuilder
func zoomable(
minZoomScale: CGFloat = 1,
doubleTapZoomScale: CGFloat = 3
) -> some View {
modifier(ZoomableModifier(
minZoomScale: minZoomScale,
doubleTapZoomScale: doubleTapZoomScale
))
}
@ViewBuilder
func zoomable(
minZoomScale: CGFloat = 1,
doubleTapZoomScale: CGFloat = 3,
outOfBoundsColor: Color = .clear
) -> some View {
GeometryReader { proxy in
ZStack {
outOfBoundsColor
self.zoomable(
minZoomScale: minZoomScale,
doubleTapZoomScale: doubleTapZoomScale
)
}
}
}
}
private extension View {
@ViewBuilder
func modify(@ViewBuilder _ fn: (Self) -> some View) -> some View {
fn(self)
}
@ViewBuilder
func animatableTransformEffect(_ transform: CGAffineTransform) -> some View {
scaleEffect(
x: transform.scaleX,
y: transform.scaleY,
anchor: .zero
)
.offset(x: transform.tx, y: transform.ty)
}
}
private extension UnitPoint {
func scaledBy(_ size: CGSize) -> CGPoint {
.init(
x: x * size.width,
y: y * size.height
)
}
}
private extension CGAffineTransform {
static func anchoredScale(scale: CGFloat, anchor: CGPoint) -> CGAffineTransform {
CGAffineTransform(translationX: anchor.x, y: anchor.y)
.scaledBy(x: scale, y: scale)
.translatedBy(x: -anchor.x, y: -anchor.y)
}
var scaleX: CGFloat {
sqrt(a * a + c * c)
}
var scaleY: CGFloat {
sqrt(b * b + d * d)
}
}
#endif