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