Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions V2er.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
0B1A2B3C4D5E6F7081920A03 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1A2B3C4D5E6F7081920A04 /* SplashView.swift */; };
0B1A2B3C4D5E6F7081920A07 /* TypewriterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1A2B3C4D5E6F7081920A08 /* TypewriterView.swift */; };
1AEBC3AC5DAA63523F5448F5 /* RichContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E205F350A3537A3E41B1AFC3 /* RichContentView.swift */; };
28B24CA92EA3460D00F82B2A /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B24CA82EA3460D00F82B2A /* BalanceView.swift */; };
28B24CAB2EA3561400F82B2A /* OnlineStatsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B24CAA2EA3561400F82B2A /* OnlineStatsInfo.swift */; };
Expand Down Expand Up @@ -184,6 +186,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
0B1A2B3C4D5E6F7081920A04 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
0B1A2B3C4D5E6F7081920A08 /* TypewriterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypewriterView.swift; sourceTree = "<group>"; };
28B24CA82EA3460D00F82B2A /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = "<group>"; };
28B24CAA2EA3561400F82B2A /* OnlineStatsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineStatsInfo.swift; sourceTree = "<group>"; };
31C4B81E79369CDE4880B773 /* RichContentView+Preview.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RichContentView+Preview.swift"; path = "V2er/Sources/RichView/Views/RichContentView+Preview.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -718,6 +722,15 @@
path = FeedDetail;
sourceTree = "<group>";
};
0B1A2B3C4D5E6F7081920A06 /* Splash */ = {
isa = PBXGroup;
children = (
0B1A2B3C4D5E6F7081920A04 /* SplashView.swift */,
0B1A2B3C4D5E6F7081920A08 /* TypewriterView.swift */,
);
path = Splash;
sourceTree = "<group>";
};
5DE5B4C826845F4F00569684 /* View */ = {
isa = PBXGroup;
children = (
Expand All @@ -734,6 +747,7 @@
5D1D7B8526FC9AF6008E0C08 /* Login */,
5D843E9626A46CB800C47D95 /* Message */,
5D179BFD2496F6EC00E40E90 /* Widget */,
0B1A2B3C4D5E6F7081920A06 /* Splash */,
4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */,
);
path = View;
Expand Down Expand Up @@ -975,6 +989,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0B1A2B3C4D5E6F7081920A03 /* SplashView.swift in Sources */,
0B1A2B3C4D5E6F7081920A07 /* TypewriterView.swift in Sources */,
5D73FBDA27284ADB004558E9 /* RichText.swift in Sources */,
5D2DD00A26FB443D0001C85A /* GlobalActions.swift in Sources */,
5D71DF57247C153C00B53ED4 /* ExplorePage.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.078",
"green" : "0.071",
"red" : "0.067"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
16 changes: 16 additions & 0 deletions V2er/Assets.xcassets/SplashLogo.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "splash_logo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Binary file not shown.
34 changes: 22 additions & 12 deletions V2er/General/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,30 @@ struct RootHostView: View {
$store.appState.loginState
}

var launchFinished: Bool {
store.appState.globalState.launchFinished
}

var body: some View {
MainPage()
.buttonStyle(.plain)
.toast(isPresented: toast.isPresented) {
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
}
.sheet(isPresented: loginState.showLoginView) {
LoginPage()
}
.overlay {
if loginState.raw.showTwoStepDialog {
TwoStepLoginPage()
ZStack {
MainPage()
.buttonStyle(.plain)
.toast(isPresented: toast.isPresented) {
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
}
.sheet(isPresented: loginState.showLoginView) {
LoginPage()
}
.overlay {
if loginState.raw.showTwoStepDialog {
TwoStepLoginPage()
}
}
}

if !launchFinished {
SplashView()
.transition(.opacity)
}
}
}
}
7 changes: 7 additions & 0 deletions V2er/State/DataFlow/Actions/GlobalActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ struct ShowToastAction: Action {
var icon: String = .empty
}

struct LaunchFinishedAction: Action {
var target: Reducer = R
}


func globalStateReducer(_ state: GlobalState, _ action: Action?) -> (GlobalState, Action?) {
var state = state
Expand All @@ -50,6 +54,9 @@ func globalStateReducer(_ state: GlobalState, _ action: Action?) -> (GlobalState
state.toast.icon = action.icon
state.toast.isPresented = true
break
case _ as LaunchFinishedAction:
state.launchFinished = true
break
default:
break
}
Expand Down
1 change: 1 addition & 0 deletions V2er/State/DataFlow/State/GlobalState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct GlobalState: FluxState {
var lastSelectedTab: TabId = .none
var scrollTopTab: TabId = .none
var toast = Toast()
var launchFinished: Bool = false

static var account: AccountInfo? {
AccountState.getAccount()
Expand Down
62 changes: 62 additions & 0 deletions V2er/View/Splash/SplashView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// SplashView.swift
// V2er
//
// Created by Claude on 2024/12/1.
//

import SwiftUI

struct SplashView: View {
@EnvironmentObject private var store: Store
@Environment(\.colorScheme) private var colorScheme

@State private var showSlogan = false

private let slogan = "Way to explore"

// Logo color adapts to color scheme (matches Android)
private var logoColor: Color {
colorScheme == .dark ? .white : Color(red: 0.067, green: 0.071, blue: 0.078)
}

var body: some View {
ZStack {
// Background color - matches Android implementation
Color("SplashBackground")
.ignoresSafeArea()

// Logo - vector PDF with template rendering (fixed position)
Image("SplashLogo")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.foregroundColor(logoColor)

// Slogan with typewriter effect (fixed position below logo)
if showSlogan {
TypewriterView(text: slogan, typingDelay: .milliseconds(35))
.font(.system(size: 17, weight: .semibold, design: .default))
.foregroundColor(logoColor.opacity(0.85))
.offset(y: 74)
}
}
.onAppear {
// Show slogan after a short delay
runInMain(delay: 300) {
showSlogan = true
}

// Hide splash after animation completes
runInMain(delay: 1200) {
store.dispatch(LaunchFinishedAction(), animation: .easeOut(duration: 0.3))
}
}
}
}

#Preview {
SplashView()
.environmentObject(Store.shared)
}
82 changes: 82 additions & 0 deletions V2er/View/Splash/TypewriterView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// TypewriterView.swift
// V2er
//
// Created by Claude on 2024/12/1.
//

import SwiftUI

struct TypewriterView: View {
var text: String
var typingDelay: Duration = .milliseconds(50)
var easeIn: Bool = true

@State private var animatedText: AttributedString = ""
@State private var typingTask: Task<Void, Never>?
@State private var hasAppeared = false

var body: some View {
Text(animatedText)
.onChange(of: text) { _ in
if hasAppeared {
animateText()
}
}
.onAppear() {
animateText()
hasAppeared = true
}
}

private func animateText() {
typingTask?.cancel()

typingTask = Task {
let defaultAttributes = AttributeContainer()
animatedText = AttributedString(text,
attributes: defaultAttributes.foregroundColor(.clear)
)

let totalChars = text.count
var charIndex = 0
var index = animatedText.startIndex

while index < animatedText.endIndex {
guard !Task.isCancelled else { return }

// Update the style
animatedText[animatedText.startIndex...index]
.setAttributes(defaultAttributes)

// Calculate delay with ease-out effect (starts fast, slows down)
let delay: Duration
if easeIn && totalChars > 1 {
// Ease-out: start fast, end slow - more natural typing feel
let progress = Double(charIndex) / Double(totalChars - 1)
let easeOutProgress = 1 - pow(1 - progress, 2) // quadratic ease-out
let baseDelay = Double(typingDelay.components.attoseconds) / 1_000_000_000_000_000_000
let minDelay = baseDelay * 0.6
let maxDelay = baseDelay * 1.5
let currentDelay = minDelay + (maxDelay - minDelay) * easeOutProgress
delay = .milliseconds(Int(currentDelay * 1000))
} else {
delay = typingDelay
}

// Wait
try? await Task.sleep(for: delay)

// Advance the index, character by character
index = animatedText.index(afterCharacter: index)
charIndex += 1
}
}
}
}

#Preview {
TypewriterView(text: "Way to explore")
.font(.title)
.padding()
}
Loading