341 lines
12 KiB
Swift
341 lines
12 KiB
Swift
import SwiftUI
|
|
|
|
struct CollectionView: View {
|
|
@EnvironmentObject var authManager: AuthManager
|
|
@EnvironmentObject var cardManager: CardManager
|
|
@State private var selectedCard: Card?
|
|
@State private var searchText = ""
|
|
@State private var selectedRarity: CardRarity?
|
|
@State private var showingFilters = false
|
|
|
|
var filteredCards: [Card] {
|
|
var cards = cardManager.userCards
|
|
|
|
// Search filter
|
|
if !searchText.isEmpty {
|
|
cards = cards.filter { card in
|
|
let cardInfo = CardInfo.all[card.id]
|
|
return cardInfo?.name.localizedCaseInsensitiveContains(searchText) ?? false
|
|
}
|
|
}
|
|
|
|
// Rarity filter
|
|
if let selectedRarity = selectedRarity {
|
|
cards = cards.filter { $0.status == selectedRarity }
|
|
}
|
|
|
|
return cards.sorted { $0.obtainedAt > $1.obtainedAt }
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ZStack {
|
|
// Background
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(hex: "0a0a0a"),
|
|
Color(hex: "1a1a1a")
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: 0) {
|
|
// Search and filter bar
|
|
VStack(spacing: 12) {
|
|
HStack {
|
|
// Search field
|
|
HStack {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundColor(.secondary)
|
|
|
|
TextField("カードを検索...", text: $searchText)
|
|
.textFieldStyle(PlainTextFieldStyle())
|
|
|
|
if !searchText.isEmpty {
|
|
Button(action: {
|
|
searchText = ""
|
|
}) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(12)
|
|
.background(Color.white.opacity(0.1))
|
|
.cornerRadius(12)
|
|
|
|
// Filter button
|
|
Button(action: {
|
|
showingFilters.toggle()
|
|
}) {
|
|
Image(systemName: "line.3.horizontal.decrease.circle")
|
|
.font(.title2)
|
|
.foregroundColor(Color(hex: "fff700"))
|
|
}
|
|
}
|
|
|
|
// Filter chips
|
|
if showingFilters {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
FilterChip(
|
|
title: "すべて",
|
|
isSelected: selectedRarity == nil
|
|
) {
|
|
selectedRarity = nil
|
|
}
|
|
|
|
ForEach(CardRarity.allCases, id: \.self) { rarity in
|
|
FilterChip(
|
|
title: rarity.displayName,
|
|
isSelected: selectedRarity == rarity
|
|
) {
|
|
selectedRarity = selectedRarity == rarity ? nil : rarity
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.top)
|
|
|
|
// Collection stats
|
|
CollectionStatsView(cards: cardManager.userCards)
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
|
|
// Card grid
|
|
if cardManager.isLoading {
|
|
Spacer()
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: Color(hex: "fff700")))
|
|
Spacer()
|
|
} else if filteredCards.isEmpty {
|
|
Spacer()
|
|
EmptyCollectionView(hasCards: !cardManager.userCards.isEmpty)
|
|
Spacer()
|
|
} else {
|
|
ScrollView {
|
|
LazyVGrid(
|
|
columns: [
|
|
GridItem(.flexible(), spacing: 16),
|
|
GridItem(.flexible(), spacing: 16)
|
|
],
|
|
spacing: 20
|
|
) {
|
|
ForEach(filteredCards) { card in
|
|
CardView(card: card)
|
|
.scaleEffect(0.8)
|
|
.onTapGesture {
|
|
selectedCard = card
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.bottom, 100)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("コレクション")
|
|
.navigationBarTitleDisplayMode(.large)
|
|
.onAppear {
|
|
if let userDid = authManager.currentUser?.did {
|
|
cardManager.loadUserCards(userDid: userDid)
|
|
}
|
|
}
|
|
.sheet(item: $selectedCard) { card in
|
|
CardDetailView(card: card)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FilterChip: View {
|
|
let title: String
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
Text(title)
|
|
.font(.caption)
|
|
.fontWeight(isSelected ? .bold : .medium)
|
|
.foregroundColor(isSelected ? .black : .white)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(
|
|
isSelected
|
|
? Color(hex: "fff700")
|
|
: Color.white.opacity(0.1)
|
|
)
|
|
.cornerRadius(16)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CollectionStatsView: View {
|
|
let cards: [Card]
|
|
|
|
private var stats: (total: Int, unique: Int, completion: Double) {
|
|
let total = cards.count
|
|
let uniqueCards = Set(cards.map { $0.id }).count
|
|
let completion = Double(uniqueCards) / 16.0 * 100
|
|
|
|
return (total, uniqueCards, completion)
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 20) {
|
|
StatItem(title: "総枚数", value: "\(stats.total)")
|
|
StatItem(title: "種類", value: "\(stats.unique)/16")
|
|
StatItem(title: "完成度", value: String(format: "%.1f%%", stats.completion))
|
|
}
|
|
.padding(.vertical, 12)
|
|
.padding(.horizontal, 16)
|
|
.background(Color.white.opacity(0.05))
|
|
.cornerRadius(12)
|
|
}
|
|
}
|
|
|
|
struct StatItem: View {
|
|
let title: String
|
|
let value: String
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
Text(value)
|
|
.font(.headline)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(Color(hex: "fff700"))
|
|
|
|
Text(title)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct EmptyCollectionView: View {
|
|
let hasCards: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: hasCards ? "magnifyingglass" : "square.stack.3d.up")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(hasCards ? "検索結果がありません" : "カードがありません")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(.white)
|
|
|
|
Text(hasCards ? "検索条件を変更してください" : "ガチャでカードを引いてみましょう")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
struct CardDetailView: View {
|
|
let card: Card
|
|
@Environment(\.presentationMode) var presentationMode
|
|
|
|
private var cardInfo: CardInfo {
|
|
CardInfo.all[card.id] ?? CardInfo(id: card.id, name: "Unknown", color: "666666", description: "")
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ZStack {
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(hex: "0a0a0a"),
|
|
Color(hex: "1a1a1a")
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
// Card display
|
|
CardView(card: card)
|
|
.scaleEffect(1.2)
|
|
.padding(.top, 20)
|
|
|
|
// Card details
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
DetailRow(title: "ID", value: "#\(card.id)")
|
|
DetailRow(title: "名前", value: cardInfo.name)
|
|
DetailRow(title: "CP", value: "\(card.cp)")
|
|
DetailRow(title: "レアリティ", value: card.status.displayName)
|
|
|
|
if let skill = card.skill, !skill.isEmpty {
|
|
DetailRow(title: "スキル", value: skill)
|
|
}
|
|
|
|
if card.isUnique, let uniqueId = card.uniqueId {
|
|
DetailRow(title: "ユニークID", value: uniqueId)
|
|
}
|
|
|
|
DetailRow(
|
|
title: "取得日時",
|
|
value: DateFormatter.localizedString(
|
|
from: card.obtainedAt,
|
|
dateStyle: .medium,
|
|
timeStyle: .short
|
|
)
|
|
)
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
.padding(.bottom, 100)
|
|
}
|
|
}
|
|
.navigationTitle(cardInfo.name)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationBarItems(
|
|
trailing: Button("閉じる") {
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DetailRow: View {
|
|
let title: String
|
|
let value: String
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text(title)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
|
|
Text(value)
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
struct CollectionView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
CollectionView()
|
|
.environmentObject(AuthManager())
|
|
.environmentObject(CardManager())
|
|
}
|
|
} |