mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
parent
e06751d458
commit
0b9dae6b98
5 changed files with 15 additions and 329 deletions
|
|
@ -2,13 +2,8 @@
|
|||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.231",
|
||||
"green" : "0.620",
|
||||
"red" : "0.976"
|
||||
}
|
||||
"platform" : "universal",
|
||||
"reference" : "systemMintColor"
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
|
|
@ -16,8 +11,5 @@
|
|||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,28 +9,20 @@ import SwiftUI
|
|||
import Kingfisher
|
||||
|
||||
struct ContentView: View {
|
||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if !hasCompletedOnboarding {
|
||||
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
|
||||
} else {
|
||||
TabView {
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "books.vertical")
|
||||
}
|
||||
SearchView()
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
TabView {
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "books.vertical")
|
||||
}
|
||||
SearchView()
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,295 +0,0 @@
|
|||
//
|
||||
// OnBoardingView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 25/03/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OnboardingView: View {
|
||||
@Binding var hasCompletedOnboarding: Bool
|
||||
@State private var currentPage = 0
|
||||
|
||||
@EnvironmentObject var settings: Settings
|
||||
|
||||
private var totalPages: Int {
|
||||
onboardingScreens.count + 1
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(.systemBackground)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
hasCompletedOnboarding = true
|
||||
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
|
||||
}
|
||||
}) {
|
||||
Text("Skip")
|
||||
.foregroundColor(.accentColor)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
TabView(selection: $currentPage) {
|
||||
ForEach(0..<totalPages, id: \.self) { index in
|
||||
if index < onboardingScreens.count {
|
||||
OnboardingPageView(page: onboardingScreens[index])
|
||||
.tag(index)
|
||||
} else if index == onboardingScreens.count {
|
||||
OnboardingCustomizeAppearanceView()
|
||||
.tag(index)
|
||||
.environmentObject(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
|
||||
PageIndicatorView(currentPage: currentPage, totalPages: totalPages)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
if currentPage > 0 {
|
||||
Button(action: {
|
||||
withAnimation { currentPage -= 1 }
|
||||
}) {
|
||||
Text("Back")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding()
|
||||
}
|
||||
Spacer()
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
if currentPage == totalPages - 1 {
|
||||
hasCompletedOnboarding = true
|
||||
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
|
||||
} else {
|
||||
currentPage += 1
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(currentPage == totalPages - 1 ? "Get Started" : "Continue")
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle())
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PageIndicatorView: View {
|
||||
let currentPage: Int
|
||||
let totalPages: Int
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<totalPages, id: \.self) { index in
|
||||
Image(systemName: currentPage == index ? "circle.fill" : "circle")
|
||||
.foregroundColor(currentPage == index ? .accentColor : .gray)
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingPageView: View {
|
||||
let page: OnboardingPage
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: page.imageName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 175, height: 175)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding()
|
||||
|
||||
Text(page.title)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(page.description)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct FilledButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.accentColor)
|
||||
.cornerRadius(12)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||
.animation(.spring(), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingPage {
|
||||
let imageName: String
|
||||
let title: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
let onboardingScreens = [
|
||||
OnboardingPage(
|
||||
imageName: "puzzlepiece.fill",
|
||||
title: "Modular Web Scraping",
|
||||
description: "Sora is a powerful, open-source web scraping app that works exclusively with custom modules."
|
||||
),
|
||||
OnboardingPage(
|
||||
imageName: "display",
|
||||
title: "Multi-Platform Support",
|
||||
description: "Enjoy Sora on iOS, iPadOS 15.0+ and macOS 12.0+. A flexible app for all your devices."
|
||||
),
|
||||
OnboardingPage(
|
||||
imageName: "play.circle.fill",
|
||||
title: "Diverse Media Playback",
|
||||
description: "Stream content from Jellyfin/Plex servers or any module and play media in external players like VLC, Infuse, and nPlayer or directly with the Sora or iOS player"
|
||||
),
|
||||
OnboardingPage(
|
||||
imageName: "lock.shield.fill",
|
||||
title: "Privacy First",
|
||||
description: "No subscriptions, no logins, no data collection. Sora prioritizes your privacy and will always be free and open source under the GPLv3.0 License."
|
||||
)
|
||||
]
|
||||
|
||||
struct OnboardingCustomizeAppearanceView: View {
|
||||
@EnvironmentObject var settings: Settings
|
||||
|
||||
@AppStorage("alwaysLandscape") private var isAlwaysLandscape = false
|
||||
|
||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Sora"
|
||||
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
|
||||
private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Customize Sora")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top, 20)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
SettingsSection(title: "Theme") {
|
||||
ColorPicker("Accent Color", selection: $settings.accentColor)
|
||||
|
||||
Picker("Appearance", selection: $settings.selectedAppearance) {
|
||||
Text("System").tag(Appearance.system)
|
||||
Text("Light").tag(Appearance.light)
|
||||
Text("Dark").tag(Appearance.dark)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
}
|
||||
|
||||
SettingsSection(title: "Media Player") {
|
||||
HStack {
|
||||
Text("Media Player")
|
||||
Spacer()
|
||||
Menu(externalPlayer) {
|
||||
ForEach(mediaPlayers, id: \.self) { provider in
|
||||
Button(action: {
|
||||
externalPlayer = provider
|
||||
}) {
|
||||
Text(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Force Landscape", isOn: $isAlwaysLandscape)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
|
||||
SettingsSection(title: "Grid Layout") {
|
||||
HStack {
|
||||
Text("Portrait Columns")
|
||||
Spacer()
|
||||
Picker("", selection: $mediaColumnsPortrait) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
ForEach(1..<6) { i in
|
||||
Text("\(i)").tag(i)
|
||||
}
|
||||
} else {
|
||||
ForEach(1..<5) { i in
|
||||
Text("\(i)").tag(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
.labelsHidden()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Landscape Columns")
|
||||
Spacer()
|
||||
Picker("", selection: $mediaColumnsLandscape) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
ForEach(2..<9) { i in
|
||||
Text("\(i)").tag(i)
|
||||
}
|
||||
} else {
|
||||
ForEach(2..<6) { i in
|
||||
Text("\(i)").tag(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(Color(UIColor.secondarySystemBackground))
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsSection<Content: View>: View {
|
||||
let title: String
|
||||
let content: Content
|
||||
|
||||
init(title: String, @ViewBuilder content: () -> Content) {
|
||||
self.title = title
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
Text(title + ":")
|
||||
.font(.headline)
|
||||
|
||||
content
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ struct SettingsViewGeneral: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@
|
|||
139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; };
|
||||
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; };
|
||||
1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; };
|
||||
13ADAE022D92ED46007DCE7D /* OnBoardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */; };
|
||||
13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; };
|
||||
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; };
|
||||
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */; };
|
||||
|
|
@ -100,7 +99,6 @@
|
|||
139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = "<group>"; };
|
||||
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewLogger.swift; sourceTree = "<group>"; };
|
||||
1399FAD52D3AB3DB00E97C31 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingView.swift; sourceTree = "<group>"; };
|
||||
13B7F4C02D58FFDD0045714A /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = "<group>"; };
|
||||
13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingManager.swift; sourceTree = "<group>"; };
|
||||
13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingItem.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -256,7 +254,6 @@
|
|||
133F55B92D33B53E00E08EEA /* LibraryView */,
|
||||
133D7C7C2D2BE2630075467E /* SearchView.swift */,
|
||||
130217CB2D81C55E0011EFF5 /* DownloadView.swift */,
|
||||
13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -528,7 +525,6 @@
|
|||
139935662D468C450065CEFF /* ModuleManager.swift in Sources */,
|
||||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
||||
1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */,
|
||||
13ADAE022D92ED46007DCE7D /* OnBoardingView.swift in Sources */,
|
||||
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */,
|
||||
13DB46902D900A38008CBC03 /* URL.swift in Sources */,
|
||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue