mirror of
https://github.com/wahyd4/image-flow.git
synced 2026-08-08 20:56:05 +10:00
Init project with windsurf
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
|
||||
# macOS specific files
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
@@ -0,0 +1,37 @@
|
||||
# Image Flow
|
||||
|
||||
A beautiful iOS app that combines your photos with stylish clock displays for a personalized fullscreen slideshow experience.
|
||||
|
||||
## Features
|
||||
|
||||
- Select multiple photos from your photo library
|
||||
- Choose from different clock styles (Digital, Analog, Minimal)
|
||||
- Select transition animations between photos (Fade, Slide, Zoom)
|
||||
- Fullscreen slideshow mode with automatic photo transitions every 10 seconds
|
||||
- Tap to exit fullscreen mode
|
||||
- Device stays awake during slideshow
|
||||
|
||||
## Requirements
|
||||
|
||||
- iOS 16.0+
|
||||
- Xcode 15.0+
|
||||
- Swift 5.9+
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Open the project in Xcode
|
||||
3. Build and run on your iOS device or simulator
|
||||
|
||||
## Usage
|
||||
|
||||
1. Launch the app
|
||||
2. Tap "Select Photos" to choose images from your photo library
|
||||
3. Select a clock style (Digital, Analog, or Minimal)
|
||||
4. Choose a transition animation (Fade, Slide, or Zoom)
|
||||
5. Tap "Start Slideshow" to enter fullscreen mode
|
||||
6. Tap anywhere on the screen to exit fullscreen mode
|
||||
|
||||
## Privacy
|
||||
|
||||
The app requires access to your photo library to display selected images in the slideshow. Photos are only accessed within the app and are not shared or stored externally.
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// ClockPreviewView.swift
|
||||
// image-flow
|
||||
//
|
||||
// Created by Junv on 1/3/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ClockPreviewView: View {
|
||||
let clockStyle: ClockStyle
|
||||
@State private var currentTime = Date()
|
||||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.blue.opacity(0.2))
|
||||
.frame(height: 100)
|
||||
|
||||
Group {
|
||||
switch clockStyle {
|
||||
case .digital:
|
||||
digitalPreview
|
||||
case .analog:
|
||||
analogPreview
|
||||
case .minimal:
|
||||
minimalPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
currentTime = Date()
|
||||
}
|
||||
}
|
||||
|
||||
private var digitalPreview: some View {
|
||||
VStack(spacing: 2) {
|
||||
Text(timeFormatter.string(from: currentTime))
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(dateFormatter.string(from: currentTime))
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var analogPreview: some View {
|
||||
ZStack {
|
||||
// Clock face
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 70, height: 70)
|
||||
.shadow(radius: 2)
|
||||
|
||||
// Hour markers
|
||||
ForEach(0..<12) { hour in
|
||||
Rectangle()
|
||||
.fill(Color.black)
|
||||
.frame(width: 2, height: hour % 3 == 0 ? 6 : 3)
|
||||
.offset(y: -30)
|
||||
.rotationEffect(.degrees(Double(hour) * 30))
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
Rectangle()
|
||||
.fill(Color.black)
|
||||
.frame(width: 2, height: 20)
|
||||
.cornerRadius(1)
|
||||
.offset(y: -10)
|
||||
.rotationEffect(.degrees(Double(hour) * 360))
|
||||
|
||||
// Minute hand
|
||||
Rectangle()
|
||||
.fill(Color.black)
|
||||
.frame(width: 1.5, height: 28)
|
||||
.cornerRadius(0.75)
|
||||
.offset(y: -14)
|
||||
.rotationEffect(.degrees(Double(minute) * 360))
|
||||
|
||||
// Second hand
|
||||
Rectangle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 1, height: 30)
|
||||
.cornerRadius(0.5)
|
||||
.offset(y: -15)
|
||||
.rotationEffect(.degrees(Double(second) * 360))
|
||||
|
||||
// Center cap
|
||||
Circle()
|
||||
.fill(Color.black)
|
||||
.frame(width: 4, height: 4)
|
||||
}
|
||||
}
|
||||
|
||||
private var minimalPreview: some View {
|
||||
Text(minimalTimeFormatter.string(from: currentTime))
|
||||
.font(.system(size: 36, weight: .thin, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
// MARK: - Helper Properties
|
||||
|
||||
private var timeFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss"
|
||||
return formatter
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, MMM d"
|
||||
return formatter
|
||||
}
|
||||
|
||||
private var minimalTimeFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter
|
||||
}
|
||||
|
||||
private var calendar: Calendar {
|
||||
return Calendar.current
|
||||
}
|
||||
|
||||
private var hour: CGFloat {
|
||||
let hour = CGFloat(calendar.component(.hour, from: currentTime) % 12)
|
||||
let minute = CGFloat(calendar.component(.minute, from: currentTime))
|
||||
return (hour + minute / 60) / 12
|
||||
}
|
||||
|
||||
private var minute: CGFloat {
|
||||
let minute = CGFloat(calendar.component(.minute, from: currentTime))
|
||||
let second = CGFloat(calendar.component(.second, from: currentTime))
|
||||
return (minute + second / 60) / 60
|
||||
}
|
||||
|
||||
private var second: CGFloat {
|
||||
let second = CGFloat(calendar.component(.second, from: currentTime))
|
||||
return second / 60
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 20) {
|
||||
ClockPreviewView(clockStyle: .digital)
|
||||
ClockPreviewView(clockStyle: .analog)
|
||||
ClockPreviewView(clockStyle: .minimal)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -6,16 +6,212 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var photoManager = PhotoPickerManager()
|
||||
@State private var clockStyle: ClockStyle = .digital
|
||||
@State private var isFullscreen = false
|
||||
@State private var currentImageIndex = 0
|
||||
@State private var transitionAnimation: TransitionAnimation = .fade
|
||||
@State private var showingPermissionAlert = false
|
||||
|
||||
let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
if isFullscreen {
|
||||
SlideshowView(
|
||||
images: photoManager.selectedImages,
|
||||
clockStyle: clockStyle,
|
||||
currentIndex: $currentImageIndex,
|
||||
animation: transitionAnimation
|
||||
)
|
||||
.statusBar(hidden: true)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture {
|
||||
isFullscreen = false
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
withAnimation {
|
||||
currentImageIndex = (currentImageIndex + 1) % max(photoManager.selectedImages.count, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("Select Photos") {
|
||||
PhotosPicker(
|
||||
selection: $photoManager.selectedItems,
|
||||
matching: .images,
|
||||
photoLibrary: .shared()
|
||||
) {
|
||||
HStack {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
Text("Select Photos")
|
||||
}
|
||||
}
|
||||
.onChange(of: photoManager.selectedItems) { _ in
|
||||
Task {
|
||||
await photoManager.loadImages()
|
||||
}
|
||||
}
|
||||
|
||||
if photoManager.isLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
} else if !photoManager.selectedImages.isEmpty {
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack {
|
||||
ForEach(0..<photoManager.selectedImages.count, id: \.self) { index in
|
||||
Image(uiImage: photoManager.selectedImages[index])
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.frame(height: 120)
|
||||
|
||||
Button(action: {
|
||||
photoManager.clearSelection()
|
||||
}) {
|
||||
Text("Clear Selection")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Clock Style") {
|
||||
Picker("Clock Style", selection: $clockStyle) {
|
||||
ForEach(ClockStyle.allCases, id: \.self) { style in
|
||||
Text(style.displayName).tag(style)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
ClockPreviewView(clockStyle: clockStyle)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section("Transition Animation") {
|
||||
Picker("Animation", selection: $transitionAnimation) {
|
||||
ForEach(TransitionAnimation.allCases, id: \.self) { animation in
|
||||
Text(animation.displayName).tag(animation)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
// Animation preview
|
||||
HStack {
|
||||
Image(systemName: "photo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
Image(systemName: getTransitionIcon())
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
|
||||
Image(systemName: "photo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: {
|
||||
if !photoManager.selectedImages.isEmpty {
|
||||
Task {
|
||||
let status = photoManager.checkPhotoLibraryPermission()
|
||||
if status == .authorized {
|
||||
isFullscreen = true
|
||||
} else if status == .notDetermined {
|
||||
let newStatus = await photoManager.requestPhotoLibraryPermission()
|
||||
if newStatus == .authorized {
|
||||
isFullscreen = true
|
||||
} else {
|
||||
showingPermissionAlert = true
|
||||
}
|
||||
} else {
|
||||
showingPermissionAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text("Start Slideshow")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(photoManager.selectedImages.isEmpty ? Color.gray : Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.disabled(photoManager.selectedImages.isEmpty)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Image Flow")
|
||||
.alert("Photo Library Access", isPresented: $showingPermissionAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
Button("Open Settings") {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("Please allow access to your photo library in Settings to use this feature.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getTransitionIcon() -> String {
|
||||
switch transitionAnimation {
|
||||
case .fade:
|
||||
return "arrow.left.and.right.square"
|
||||
case .slide:
|
||||
return "arrow.right.square"
|
||||
case .zoom:
|
||||
return "arrow.up.left.and.arrow.down.right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ClockStyle: String, CaseIterable {
|
||||
case digital
|
||||
case analog
|
||||
case minimal
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .digital: return "Digital"
|
||||
case .analog: return "Analog"
|
||||
case .minimal: return "Minimal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TransitionAnimation: String, CaseIterable {
|
||||
case fade
|
||||
case slide
|
||||
case zoom
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .fade: return "Fade"
|
||||
case .slide: return "Slide"
|
||||
case .zoom: return "Zoom"
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs access to your photo library to display selected images in the slideshow.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="gfx-5g-7RV">
|
||||
<rect key="frame" x="96.666666666666686" y="363.66666666666669" width="200" height="125"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="photo.on.rectangle.angled" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="XYw-Iq-Fzd">
|
||||
<rect key="frame" x="50" y="0.6666666666666643" width="100" height="79"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="100" id="Lfg-Yd-Xvf"/>
|
||||
<constraint firstAttribute="height" constant="80" id="eTY-5h-GRb"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Image Flow" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zzh-Gu-Hkl">
|
||||
<rect key="frame" x="34" y="96" width="132" height="29"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="Ygf-Oe-lYL"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="gfx-5g-7RV" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="Qjw-Gy-Ufr"/>
|
||||
<constraint firstItem="gfx-5g-7RV" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="pNV-Yd-xgK"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="photo.on.rectangle.angled" catalog="system" width="128" height="98"/>
|
||||
<systemColor name="systemBlueColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// PhotoPickerManager.swift
|
||||
// image-flow
|
||||
//
|
||||
// Created by Junv on 1/3/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
class PhotoPickerManager: ObservableObject {
|
||||
@Published var selectedItems: [PhotosPickerItem] = []
|
||||
@Published var selectedImages: [UIImage] = []
|
||||
@Published var isLoading = false
|
||||
|
||||
func loadImages() async {
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = true
|
||||
}
|
||||
|
||||
var newImages: [UIImage] = []
|
||||
|
||||
for item in selectedItems {
|
||||
if let data = try? await item.loadTransferable(type: Data.self),
|
||||
let uiImage = UIImage(data: data) {
|
||||
newImages.append(uiImage)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.selectedImages = newImages
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func clearSelection() {
|
||||
selectedItems = []
|
||||
selectedImages = []
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to check and request photo library permission
|
||||
extension PhotoPickerManager {
|
||||
enum PhotoLibraryPermissionStatus {
|
||||
case authorized
|
||||
case denied
|
||||
case restricted
|
||||
case notDetermined
|
||||
}
|
||||
|
||||
func checkPhotoLibraryPermission() -> PhotoLibraryPermissionStatus {
|
||||
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
||||
|
||||
switch status {
|
||||
case .authorized, .limited:
|
||||
return .authorized
|
||||
case .denied:
|
||||
return .denied
|
||||
case .restricted:
|
||||
return .restricted
|
||||
case .notDetermined:
|
||||
return .notDetermined
|
||||
@unknown default:
|
||||
return .notDetermined
|
||||
}
|
||||
}
|
||||
|
||||
func requestPhotoLibraryPermission() async -> PhotoLibraryPermissionStatus {
|
||||
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
|
||||
|
||||
switch status {
|
||||
case .authorized, .limited:
|
||||
return .authorized
|
||||
case .denied:
|
||||
return .denied
|
||||
case .restricted:
|
||||
return .restricted
|
||||
case .notDetermined:
|
||||
return .notDetermined
|
||||
@unknown default:
|
||||
return .notDetermined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// SlideshowView.swift
|
||||
// image-flow
|
||||
//
|
||||
// Created by Junv on 1/3/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SlideshowView: View {
|
||||
let images: [UIImage]
|
||||
let clockStyle: ClockStyle
|
||||
@Binding var currentIndex: Int
|
||||
let animation: TransitionAnimation
|
||||
|
||||
@State private var currentTime = Date()
|
||||
@State private var previousIndex: Int?
|
||||
@State private var isAnimating = false
|
||||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background image
|
||||
if !images.isEmpty {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// Current image
|
||||
Image(uiImage: images[currentIndex])
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.clipped()
|
||||
.transition(transitionEffect())
|
||||
|
||||
// Overlay gradient for better clock visibility
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [Color.black.opacity(0.4), Color.clear]),
|
||||
startPoint: .top,
|
||||
endPoint: .center
|
||||
)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.animation(.easeInOut(duration: 1.0), value: currentIndex)
|
||||
}
|
||||
|
||||
// Clock overlay
|
||||
VStack {
|
||||
clockView
|
||||
.padding(.top, 50)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Image counter
|
||||
if images.count > 1 {
|
||||
Text("\(currentIndex + 1) / \(images.count)")
|
||||
.font(.caption)
|
||||
.padding(8)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
currentTime = Date()
|
||||
}
|
||||
}
|
||||
|
||||
private var clockView: some View {
|
||||
Group {
|
||||
switch clockStyle {
|
||||
case .digital:
|
||||
DigitalClockView(currentTime: currentTime)
|
||||
case .analog:
|
||||
AnalogClockView(currentTime: currentTime)
|
||||
case .minimal:
|
||||
MinimalClockView(currentTime: currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func transitionEffect() -> AnyTransition {
|
||||
switch animation {
|
||||
case .fade:
|
||||
return AnyTransition.opacity
|
||||
case .slide:
|
||||
return AnyTransition.asymmetric(
|
||||
insertion: .move(edge: .trailing),
|
||||
removal: .move(edge: .leading)
|
||||
)
|
||||
case .zoom:
|
||||
return AnyTransition.scale(scale: 0.8).combined(with: .opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DigitalClockView: View {
|
||||
let currentTime: Date
|
||||
|
||||
private var timeFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss"
|
||||
return formatter
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, MMMM d, yyyy"
|
||||
return formatter
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
Text(timeFormatter.string(from: currentTime))
|
||||
.font(.system(size: 60, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .black.opacity(0.5), radius: 2, x: 0, y: 1)
|
||||
|
||||
Text(dateFormatter.string(from: currentTime))
|
||||
.font(.system(size: 20, weight: .medium, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .black.opacity(0.5), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.3))
|
||||
.cornerRadius(16)
|
||||
}
|
||||
}
|
||||
|
||||
struct AnalogClockView: View {
|
||||
let currentTime: Date
|
||||
|
||||
private var calendar: Calendar {
|
||||
return Calendar.current
|
||||
}
|
||||
|
||||
private var hour: CGFloat {
|
||||
let hour = CGFloat(calendar.component(.hour, from: currentTime) % 12)
|
||||
let minute = CGFloat(calendar.component(.minute, from: currentTime))
|
||||
return (hour + minute / 60) / 12
|
||||
}
|
||||
|
||||
private var minute: CGFloat {
|
||||
let minute = CGFloat(calendar.component(.minute, from: currentTime))
|
||||
let second = CGFloat(calendar.component(.second, from: currentTime))
|
||||
return (minute + second / 60) / 60
|
||||
}
|
||||
|
||||
private var second: CGFloat {
|
||||
let second = CGFloat(calendar.component(.second, from: currentTime))
|
||||
return second / 60
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Clock face
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 200, height: 200)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.white, lineWidth: 4)
|
||||
)
|
||||
|
||||
// Hour markers
|
||||
ForEach(0..<12) { hour in
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 4, height: hour % 3 == 0 ? 15 : 8)
|
||||
.offset(y: -85)
|
||||
.rotationEffect(.degrees(Double(hour) * 30))
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 4, height: 60)
|
||||
.cornerRadius(2)
|
||||
.offset(y: -30)
|
||||
.rotationEffect(.degrees(Double(hour) * 360))
|
||||
|
||||
// Minute hand
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 3, height: 80)
|
||||
.cornerRadius(1.5)
|
||||
.offset(y: -40)
|
||||
.rotationEffect(.degrees(Double(minute) * 360))
|
||||
|
||||
// Second hand
|
||||
Rectangle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 1, height: 90)
|
||||
.cornerRadius(0.5)
|
||||
.offset(y: -45)
|
||||
.rotationEffect(.degrees(Double(second) * 360))
|
||||
|
||||
// Center cap
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
.frame(width: 220, height: 220)
|
||||
.shadow(color: .black.opacity(0.5), radius: 5, x: 0, y: 2)
|
||||
}
|
||||
}
|
||||
|
||||
struct MinimalClockView: View {
|
||||
let currentTime: Date
|
||||
|
||||
private var timeFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(timeFormatter.string(from: currentTime))
|
||||
.font(.system(size: 80, weight: .thin, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .black.opacity(0.5), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SlideshowView(
|
||||
images: [UIImage(systemName: "photo")!],
|
||||
clockStyle: .digital,
|
||||
currentIndex: .constant(0),
|
||||
animation: .fade
|
||||
)
|
||||
}
|
||||
@@ -6,12 +6,23 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
struct image_flowApp: App {
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
// Prevent screen from dimming during slideshow
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user