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.
56 lines
1.2 KiB
Swift
56 lines
1.2 KiB
Swift
//
|
|
// Date+timeLeft.swift
|
|
// Feather
|
|
//
|
|
// Created by samara on 16.04.2025.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
extension Date {
|
|
public struct ExpirationInfo {
|
|
public let formatted: String
|
|
public let color: Color
|
|
public let icon: String
|
|
}
|
|
|
|
/// Gathers data for `ExpirationInfo`
|
|
/// - Parameter now: Date
|
|
/// - Returns: `ExpirationInfo`
|
|
public func expirationInfo(from now: Date = .now) -> ExpirationInfo {
|
|
let timeLeft = self.timeIntervalSince(now)
|
|
|
|
guard timeLeft > 0 else {
|
|
return ExpirationInfo(
|
|
formatted: .localized("Expired"),
|
|
color: .gray,
|
|
icon: "xmark.octagon"
|
|
)
|
|
}
|
|
|
|
let daysLeft = Int(timeLeft / 86400)
|
|
let color = Color.expiration(days: daysLeft)
|
|
|
|
let formatter = Date._expirationFormatter(for: timeLeft)
|
|
let timeString = formatter.string(from: timeLeft) ?? .localized("%lld days", arguments: daysLeft)
|
|
|
|
return ExpirationInfo(
|
|
formatted: timeString,
|
|
color: color,
|
|
icon: "clock"
|
|
)
|
|
}
|
|
|
|
private static func _expirationFormatter(for interval: TimeInterval) -> DateComponentsFormatter {
|
|
let formatter = DateComponentsFormatter()
|
|
formatter.allowedUnits = interval < 3600
|
|
? [.minute]
|
|
: interval < 86400
|
|
? [.hour]
|
|
: [.day]
|
|
formatter.unitsStyle = .full
|
|
return formatter
|
|
}
|
|
}
|