FTStore now holds both halves of the product:
ios/ the swift app — the fork of claration/Feather, unchanged apart
from its new home
web/ FTStore Admin, formerly its own repository. serves the
dashboard at /admin, the ftstore.json feed and IPA downloads
app-repo.json and Images/ stay at the repo root on purpose: the former is
a published AltStore source whose raw.githubusercontent URL is what
installed copies resolve updates through, so moving it would break every
subscriber.
The admin server is renamed throughout: package ftstore-admin, images
ghcr.io/<owner>/ftstore-admin-{app,worker}, postgres role/db ftstore,
session cookie ftstore_session, temp dir /tmp/ftstore, backup type
ftstore-settings, outbound user-agent FTStoreAdmin. backups written under the old
name still import, and compose is pinned to name: ftstore so the
volume prefix does not follow the directory name.
apply-rebrand.sh gains a relocation step: upstream's tree is rooted at
the repo root while ours lives in ios/, so a merge drops Feather's files
next to ios/ and the script folds them in before renaming. it never
touches web/. still idempotent.
release tags are namespaced — v* for the app, web-v* for the server —
so a server release no longer triggers a macOS build or rewrites
app-repo.json. workflows are prefixed ios-/web- and path-filtered.
89 lines
1.8 KiB
Swift
89 lines
1.8 KiB
Swift
//
|
|
// CertificatesPillView.swift
|
|
// Feather
|
|
//
|
|
// Created by samara on 16.04.2025.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
public struct NBPillView: View {
|
|
public var title: String
|
|
public var icon: String
|
|
public var color: Color
|
|
public var index: Int
|
|
public var count: Int
|
|
|
|
public init(
|
|
title: String,
|
|
icon: String,
|
|
color: Color,
|
|
index: Int,
|
|
count: Int
|
|
) {
|
|
self.title = title
|
|
self.icon = icon
|
|
self.color = color
|
|
self.index = index
|
|
self.count = count
|
|
}
|
|
|
|
public var body: some View {
|
|
let position = PillPosition.position(for: index, in: count)
|
|
let radii = position.cornerRadii
|
|
|
|
HStack(spacing: 4) {
|
|
Image(systemName: icon)
|
|
.font(.caption)
|
|
.foregroundStyle(color.opacity(0.9))
|
|
Text(title)
|
|
.font(.caption.bold())
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 10)
|
|
.background(
|
|
UnevenRoundedRectangle(
|
|
cornerRadii: .init(
|
|
topLeading: radii.topLeading,
|
|
bottomLeading: radii.bottomLeading,
|
|
bottomTrailing: radii.bottomTrailing,
|
|
topTrailing: radii.topTrailing
|
|
),
|
|
style: .continuous
|
|
)
|
|
.fill(color.opacity(0.15))
|
|
)
|
|
}
|
|
|
|
public enum PillPosition {
|
|
case single, first, middle, last
|
|
|
|
static public func position(for index: Int, in count: Int) -> PillPosition {
|
|
switch count {
|
|
case 1: return .single
|
|
case _ where index == 0: return .first
|
|
case _ where index == count - 1: return .last
|
|
default: return .middle
|
|
}
|
|
}
|
|
|
|
public var cornerRadii: (topLeading: CGFloat, bottomLeading: CGFloat, bottomTrailing: CGFloat, topTrailing: CGFloat) {
|
|
if #available(iOS 26.0, *) {
|
|
return (16, 16, 16, 16)
|
|
}
|
|
else {
|
|
switch self {
|
|
case .single:
|
|
return (10, 10, 10, 10)
|
|
case .first:
|
|
return (10, 10, 5, 5)
|
|
case .middle:
|
|
return (5, 5, 5, 5)
|
|
case .last:
|
|
return (5, 5, 10, 10)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|