UI Overhaul
This commit is contained in:
parent
85cf5048ff
commit
908044fd02
44
Pomelo/Emulation/ControllerView/Gyro/MotionManager.swift
Normal file
44
Pomelo/Emulation/ControllerView/Gyro/MotionManager.swift
Normal 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()
|
||||
}
|
||||
}
|
0
Pomelo/Extentions/MTLViewExtentions.swift
Normal file
0
Pomelo/Extentions/MTLViewExtentions.swift
Normal file
88
Pomelo/LibraryViews/GameGrid/GameGridView.swift
Normal file
88
Pomelo/LibraryViews/GameGrid/GameGridView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
67
Pomelo/LibraryViews/Menu/BottomMenuView.swift
Normal file
67
Pomelo/LibraryViews/Menu/BottomMenuView.swift
Normal 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)
|
||||
}
|
||||
}
|
76
Pomelo/LibraryViews/Menu/TopBarView.swift
Normal file
76
Pomelo/LibraryViews/Menu/TopBarView.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
0
Pomelo/ScreenshotManager/ScreenShotListView.swift
Normal file
0
Pomelo/ScreenshotManager/ScreenShotListView.swift
Normal file
209
Pomelo/ScreenshotManager/Zoomable.swift
Normal file
209
Pomelo/ScreenshotManager/Zoomable.swift
Normal 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
|
Loading…
x
Reference in New Issue
Block a user