feat: enhance UI with improved label management and splash screen
- Add new logo and splash screen assets with multiple resolutions - Implement paginated label selection with TabView - Create UnifiedLabelChip component for consistent label display - Add manual tag entry functionality with validation - Refactor BookmarkLabelsViewModel with dependency injection - Update launch screen configuration and color sets - Add new localization strings for improved UX - Improve ShareBookmarkView with better label selection UI
@ -25,6 +25,9 @@
|
|||||||
},
|
},
|
||||||
"%lld min" : {
|
"%lld min" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"%lld minutes" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld." : {
|
"%lld." : {
|
||||||
|
|
||||||
@ -44,11 +47,14 @@
|
|||||||
},
|
},
|
||||||
"Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly." : {
|
"Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Add" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Add a new link to your collection" : {
|
"Add a new link to your collection" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Add new label" : {
|
"Add new tag..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"all" : {
|
"all" : {
|
||||||
@ -61,6 +67,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"All available tags" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Archive" : {
|
"Archive" : {
|
||||||
|
|
||||||
@ -73,9 +82,18 @@
|
|||||||
},
|
},
|
||||||
"Are you sure you want to log out? This will delete all your login credentials and return you to setup." : {
|
"Are you sure you want to log out? This will delete all your login credentials and return you to setup." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Automatic sync" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Automatically mark articles as read" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Cancel" : {
|
"Cancel" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Clear cache" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Clipboard" : {
|
"Clipboard" : {
|
||||||
|
|
||||||
@ -85,6 +103,9 @@
|
|||||||
},
|
},
|
||||||
"Current labels" : {
|
"Current labels" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Data Management" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Delete" : {
|
"Delete" : {
|
||||||
|
|
||||||
@ -194,10 +215,10 @@
|
|||||||
"No bookmarks found in %@." : {
|
"No bookmarks found in %@." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"No labels available" : {
|
"OK" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"Open external links in in-app Safari" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Optional: Custom title" : {
|
"Optional: Custom title" : {
|
||||||
@ -239,18 +260,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Reading Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Remove" : {
|
"Remove" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Required" : {
|
"Required" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Reset settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Restore" : {
|
"Restore" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Resume listening" : {
|
"Resume listening" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Safari Reader Mode" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Save bookmark" : {
|
"Save bookmark" : {
|
||||||
|
|
||||||
@ -263,6 +293,9 @@
|
|||||||
},
|
},
|
||||||
"Select a bookmark or tag" : {
|
"Select a bookmark or tag" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Select labels" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Server Endpoint" : {
|
"Server Endpoint" : {
|
||||||
|
|
||||||
@ -284,6 +317,12 @@
|
|||||||
},
|
},
|
||||||
"Suche..." : {
|
"Suche..." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Sync interval" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Sync Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Theme" : {
|
"Theme" : {
|
||||||
|
|
||||||
|
|||||||
@ -49,11 +49,53 @@ struct ShareBookmarkView: View {
|
|||||||
|
|
||||||
// Label Grid
|
// Label Grid
|
||||||
if !viewModel.labels.isEmpty {
|
if !viewModel.labels.isEmpty {
|
||||||
LabelGridView(labels: viewModel.labels, selected: $viewModel.selectedLabels)
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Select labels")
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
let pageSize = Constants.Labels.pageSize
|
||||||
|
let pages = stride(from: 0, to: viewModel.labels.count, by: pageSize).map {
|
||||||
|
Array(viewModel.labels[$0..<min($0 + pageSize, viewModel.labels.count)])
|
||||||
|
}
|
||||||
|
|
||||||
|
TabView {
|
||||||
|
ForEach(Array(pages.enumerated()), id: \.offset) { pageIndex, labelsPage in
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(labelsPage, id: \.name) { label in
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: label.name,
|
||||||
|
isSelected: viewModel.selectedLabels.contains(label.name),
|
||||||
|
isRemovable: false,
|
||||||
|
onTap: {
|
||||||
|
if viewModel.selectedLabels.contains(label.name) {
|
||||||
|
viewModel.selectedLabels.remove(label.name)
|
||||||
|
} else {
|
||||||
|
viewModel.selectedLabels.insert(label.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
|
||||||
|
.frame(height: 180)
|
||||||
|
.padding(.top, -20)
|
||||||
|
}
|
||||||
.padding(.top, 32)
|
.padding(.top, 32)
|
||||||
.padding(.horizontal, 16)
|
.frame(minHeight: 100)
|
||||||
.frame(minHeight: 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManualTagEntryView(
|
||||||
|
labels: viewModel.labels,
|
||||||
|
selectedLabels: $viewModel.selectedLabels
|
||||||
|
)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
if let status = viewModel.statusMessage {
|
if let status = viewModel.statusMessage {
|
||||||
Text(status.emoji + " " + status.text)
|
Text(status.emoji + " " + status.text)
|
||||||
@ -89,33 +131,45 @@ struct ShareBookmarkView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LabelGridView: View {
|
struct ManualTagEntryView: View {
|
||||||
let labels: [BookmarkLabelDto]
|
let labels: [BookmarkLabelDto]
|
||||||
@Binding var selected: Set<String>
|
@Binding var selectedLabels: Set<String>
|
||||||
private let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
|
@State private var manualTag: String = ""
|
||||||
|
@State private var error: String? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVGrid(columns: columns, spacing: 12) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
ForEach(labels.prefix(15), id: \ .name) { label in
|
HStack {
|
||||||
Button(action: {
|
TextField("Add new tag...", text: $manualTag)
|
||||||
if selected.contains(label.name) {
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
selected.remove(label.name)
|
.autocapitalization(.none)
|
||||||
} else {
|
.disableAutocorrection(true)
|
||||||
selected.insert(label.name)
|
Button(action: addTag) {
|
||||||
}
|
Text("Add")
|
||||||
}) {
|
.font(.system(size: 15, weight: .semibold))
|
||||||
Text(label.name)
|
|
||||||
.font(.system(size: 14, weight: .medium))
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(selected.contains(label.name) ? Color.accentColor.opacity(0.2) : Color(.secondarySystemGroupedBackground))
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.cornerRadius(8)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(selected.contains(label.name) ? Color.accentColor : Color.clear, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.disabled(manualTag.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
|
}
|
||||||
|
if let error = error {
|
||||||
|
Text(error)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addTag() {
|
||||||
|
let trimmed = manualTag.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return }
|
||||||
|
let lowercased = trimmed.lowercased()
|
||||||
|
let allExisting = Set(labels.map { $0.name.lowercased() })
|
||||||
|
let allSelected = Set(selectedLabels.map { $0.lowercased() })
|
||||||
|
if allExisting.contains(lowercased) || allSelected.contains(lowercased) {
|
||||||
|
error = "Tag already exists."
|
||||||
|
} else {
|
||||||
|
selectedLabels.insert(trimmed)
|
||||||
|
manualTag = ""
|
||||||
|
error = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
let loaded = await SimpleAPI.getBookmarkLabels { [weak self] message, error in
|
let loaded = await SimpleAPI.getBookmarkLabels { [weak self] message, error in
|
||||||
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
||||||
} ?? []
|
} ?? []
|
||||||
let sorted = loaded.prefix(10).sorted { $0.count > $1.count }
|
let sorted = loaded.sorted { $0.count > $1.count }
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.labels = Array(sorted)
|
self.labels = Array(sorted)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,6 +86,9 @@
|
|||||||
Data/KeychainHelper.swift,
|
Data/KeychainHelper.swift,
|
||||||
Domain/Model/Bookmark.swift,
|
Domain/Model/Bookmark.swift,
|
||||||
readeck.xcdatamodeld,
|
readeck.xcdatamodeld,
|
||||||
|
Splash.storyboard,
|
||||||
|
UI/Components/Constants.swift,
|
||||||
|
UI/Components/UnifiedLabelChip.swift,
|
||||||
);
|
);
|
||||||
target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */;
|
target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */;
|
||||||
};
|
};
|
||||||
@ -447,6 +450,10 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@ -476,6 +483,10 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
@ -23,9 +23,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x5A",
|
"blue" : "0x4B",
|
||||||
"green" : "0x4A",
|
"green" : "0x41",
|
||||||
"red" : "0x1F"
|
"red" : "0x20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|||||||
59
readeck/Assets.xcassets/logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "logo.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "logo 5.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "logo 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "logo 4.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "logo 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "logo 3.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
readeck/Assets.xcassets/logo.imageset/logo 1.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
readeck/Assets.xcassets/logo.imageset/logo 2.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
readeck/Assets.xcassets/logo.imageset/logo 3.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
readeck/Assets.xcassets/logo.imageset/logo 4.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
readeck/Assets.xcassets/logo.imageset/logo 5.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
readeck/Assets.xcassets/logo.imageset/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
26
readeck/Assets.xcassets/splash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "readeck.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "readeck 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "readeck 2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
readeck/Assets.xcassets/splash.imageset/readeck 1.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
readeck/Assets.xcassets/splash.imageset/readeck 2.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
readeck/Assets.xcassets/splash.imageset/readeck.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x60",
|
||||||
|
"green" : "0x4E",
|
||||||
|
"red" : "0x01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,8 +5,8 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x5A",
|
"blue" : "0x5B",
|
||||||
"green" : "0x4A",
|
"green" : "0x4B",
|
||||||
"red" : "0x1F"
|
"red" : "0x1F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -23,9 +23,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x5A",
|
"blue" : "0x4C",
|
||||||
"green" : "0x4A",
|
"green" : "0x40",
|
||||||
"red" : "0x1F"
|
"red" : "0x21"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|||||||
@ -25,9 +25,9 @@
|
|||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIColorName</key>
|
<key>UIColorName</key>
|
||||||
<string>splashBg</string>
|
<string>splashBackground</string>
|
||||||
<key>UIImageName</key>
|
<key>UIImageName</key>
|
||||||
<string>readeck</string>
|
<string>splash</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
67
readeck/Splash.storyboard
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="dark"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="s0d-6b-0kx">
|
||||||
|
<objects>
|
||||||
|
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logo" translatesAutoresizingMaskIntoConstraints="NO" id="NbE-6K-ltk">
|
||||||
|
<rect key="frame" x="130" y="368" width="133.33333333333337" height="116"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="133.33000000000001" id="MuN-6D-myL"/>
|
||||||
|
<constraint firstAttribute="height" constant="115.67" id="ebY-kI-orh"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TRS-0D-Iyx">
|
||||||
|
<rect key="frame" x="175" y="669" width="42" height="21"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logo" translatesAutoresizingMaskIntoConstraints="NO" id="bt9-XM-VsM">
|
||||||
|
<rect key="frame" x="155" y="59" width="82" height="80"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="82" id="mei-64-UsF"/>
|
||||||
|
<constraint firstAttribute="width" secondItem="bt9-XM-VsM" secondAttribute="height" multiplier="41:40" id="wHS-wO-Ehi"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||||
|
<color key="backgroundColor" name="BrightWhite"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="bt9-XM-VsM" secondAttribute="trailing" constant="156" id="Nqh-Mz-6ie"/>
|
||||||
|
<constraint firstItem="NbE-6K-ltk" firstAttribute="centerX" secondItem="5EZ-qb-Rvc" secondAttribute="centerX" id="S8g-oD-g1Z"/>
|
||||||
|
<constraint firstItem="bt9-XM-VsM" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" constant="59" id="X6D-aF-ddp"/>
|
||||||
|
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="TRS-0D-Iyx" secondAttribute="trailing" constant="176" id="Ybu-ZP-2KF"/>
|
||||||
|
<constraint firstItem="bt9-XM-VsM" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="155" id="Zfp-jh-UjO"/>
|
||||||
|
<constraint firstItem="TRS-0D-Iyx" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="175" id="aBg-bZ-l74"/>
|
||||||
|
<constraint firstItem="NbE-6K-ltk" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="f9e-9Z-hjf"/>
|
||||||
|
<constraint firstItem="vDu-zF-Fre" firstAttribute="bottom" secondItem="TRS-0D-Iyx" secondAttribute="bottom" constant="94" id="iFw-vd-HUs"/>
|
||||||
|
<constraint firstItem="TRS-0D-Iyx" firstAttribute="top" secondItem="NbE-6K-ltk" secondAttribute="bottom" constant="185" id="mc7-gW-Rsl"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="125.95419847328243" y="-17.605633802816904"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="logo" width="133.33332824707031" height="115.66666412353516"/>
|
||||||
|
<namedColor name="BrightWhite">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@ -5,32 +5,111 @@ struct BookmarkLabelsView: View {
|
|||||||
@State private var viewModel: BookmarkLabelsViewModel
|
@State private var viewModel: BookmarkLabelsViewModel
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
init(bookmarkId: String, initialLabels: [String]) {
|
init(bookmarkId: String, initialLabels: [String], viewModel: BookmarkLabelsViewModel? = nil) {
|
||||||
self.bookmarkId = bookmarkId
|
self.bookmarkId = bookmarkId
|
||||||
self._viewModel = State(initialValue: BookmarkLabelsViewModel(initialLabels: initialLabels))
|
self._viewModel = State(initialValue: viewModel ?? BookmarkLabelsViewModel(initialLabels: initialLabels))
|
||||||
|
|
||||||
|
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(Color.primary)
|
||||||
|
UIPageControl.appearance().pageIndicatorTintColor = UIColor(Color.primary).withAlphaComponent(0.2)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 12) {
|
||||||
// Add new label section
|
// Add new label
|
||||||
addLabelSection
|
HStack(spacing: 12) {
|
||||||
|
TextField("Enter label...", text: $viewModel.newLabelText)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.onSubmit {
|
||||||
|
Task {
|
||||||
|
await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
Task {
|
||||||
|
await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
.disabled(viewModel.newLabelText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
Divider()
|
// All available labels
|
||||||
.padding(.horizontal, -16)
|
if !viewModel.allLabels.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("All available tags")
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
TabView {
|
||||||
|
ForEach(Array(viewModel.labelPages.enumerated()), id: \ .offset) { pageIndex, labelsPage in
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(labelsPage.filter { !viewModel.currentLabels.contains($0.name) }, id: \ .id) { label in
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: label.name,
|
||||||
|
isSelected: viewModel.currentLabels.contains(label.name),
|
||||||
|
isRemovable: false,
|
||||||
|
onTap: {
|
||||||
|
Task {
|
||||||
|
await viewModel.toggleLabel(for: bookmarkId, label: label.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
|
||||||
|
.frame(height: 180)
|
||||||
|
.padding(.top, -20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Current labels section
|
// Current labels
|
||||||
currentLabelsSection
|
if !viewModel.currentLabels.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Current labels")
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(viewModel.currentLabels, id: \.self) { label in
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: label,
|
||||||
|
isSelected: true,
|
||||||
|
isRemovable: true,
|
||||||
|
onTap: {
|
||||||
|
// No action for current labels
|
||||||
|
},
|
||||||
|
onRemove: {
|
||||||
|
Task {
|
||||||
|
await viewModel.removeLabel(from: bookmarkId, label: label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.vertical)
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
.navigationTitle("Manage Labels")
|
.navigationTitle("Manage Labels")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
@ -46,131 +125,17 @@ struct BookmarkLabelsView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.errorMessage ?? "Unknown error")
|
Text(viewModel.errorMessage ?? "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
.task {
|
||||||
}
|
await viewModel.loadAllLabels()
|
||||||
|
}
|
||||||
private var addLabelSection: some View {
|
.ignoresSafeArea(.keyboard)
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
.onTapGesture {
|
||||||
Text("Add new label")
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
TextField("Enter label...", text: $viewModel.newLabelText)
|
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
|
||||||
.onSubmit {
|
|
||||||
Task {
|
|
||||||
await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
Task {
|
|
||||||
await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Image(systemName: "plus.circle.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
.background(
|
|
||||||
Circle()
|
|
||||||
.fill(Color.accentColor)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.disabled(viewModel.newLabelText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.secondarySystemGroupedBackground))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var currentLabelsSection: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Text("Current labels")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if viewModel.isLoading {
|
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(0.8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.currentLabels.isEmpty {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: "tag")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("No labels available")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.vertical, 32)
|
|
||||||
} else {
|
|
||||||
LazyVGrid(columns: [
|
|
||||||
GridItem(.adaptive(minimum: 80, maximum: 150))
|
|
||||||
], spacing: 4) {
|
|
||||||
ForEach(viewModel.currentLabels, id: \.self) { label in
|
|
||||||
LabelChip(
|
|
||||||
label: label,
|
|
||||||
onRemove: {
|
|
||||||
Task {
|
|
||||||
await viewModel.removeLabel(from: bookmarkId, label: label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(.secondarySystemGroupedBackground))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LabelChip: View {
|
|
||||||
let label: String
|
|
||||||
let onRemove: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text(label)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
|
|
||||||
Button(action: onRemove) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 10)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.fill(Color.accentColor.opacity(0.15))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 16)
|
|
||||||
.stroke(Color.accentColor.opacity(0.4), lineWidth: 1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"])
|
BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"], viewModel: .init(MockUseCaseFactory(), initialLabels: ["test"]))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import Foundation
|
|||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class BookmarkLabelsViewModel {
|
class BookmarkLabelsViewModel {
|
||||||
private let addLabelsUseCase = DefaultUseCaseFactory.shared.makeAddLabelsToBookmarkUseCase()
|
private let addLabelsUseCase: PAddLabelsToBookmarkUseCase
|
||||||
private let removeLabelsUseCase = DefaultUseCaseFactory.shared.makeRemoveLabelsFromBookmarkUseCase()
|
private let removeLabelsUseCase: PRemoveLabelsFromBookmarkUseCase
|
||||||
|
private let getLabelsUseCase: PGetLabelsUseCase
|
||||||
|
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
@ -11,8 +12,38 @@ class BookmarkLabelsViewModel {
|
|||||||
var currentLabels: [String] = []
|
var currentLabels: [String] = []
|
||||||
var newLabelText = ""
|
var newLabelText = ""
|
||||||
|
|
||||||
init(initialLabels: [String] = []) {
|
|
||||||
|
var allLabels: [BookmarkLabel] = [] {
|
||||||
|
didSet {
|
||||||
|
let pageSize = Constants.Labels.pageSize
|
||||||
|
labelPages = stride(from: 0, to: allLabels.count, by: pageSize).map {
|
||||||
|
Array(allLabels[$0..<min($0 + pageSize, allLabels.count)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelPages: [[BookmarkLabel]] = []
|
||||||
|
|
||||||
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared, initialLabels: [String] = []) {
|
||||||
self.currentLabels = initialLabels
|
self.currentLabels = initialLabels
|
||||||
|
|
||||||
|
self.addLabelsUseCase = factory.makeAddLabelsToBookmarkUseCase()
|
||||||
|
self.removeLabelsUseCase = factory.makeRemoveLabelsFromBookmarkUseCase()
|
||||||
|
self.getLabelsUseCase = factory.makeGetLabelsUseCase()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func loadAllLabels() async {
|
||||||
|
isLoading = true
|
||||||
|
defer { isLoading = false }
|
||||||
|
do {
|
||||||
|
let labels = try await getLabelsUseCase.execute()
|
||||||
|
allLabels = labels
|
||||||
|
} catch {
|
||||||
|
errorMessage = "failed to load labels"
|
||||||
|
showErrorAlert = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -83,4 +114,4 @@ class BookmarkLabelsViewModel {
|
|||||||
func updateLabels(_ labels: [String]) {
|
func updateLabels(_ labels: [String]) {
|
||||||
currentLabels = labels
|
currentLabels = labels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
readeck/UI/Components/Constants.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Constants.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 21.07.25.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// This file is part of the readeck project and is licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Constants {
|
||||||
|
struct Labels {
|
||||||
|
static let pageSize = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
97
readeck/UI/Components/UnifiedLabelChip.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// UnifiedLabelChip.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 21.07.25.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// This file is part of the readeck project and is licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UnifiedLabelChip: View {
|
||||||
|
let label: String
|
||||||
|
let isSelected: Bool
|
||||||
|
let isRemovable: Bool
|
||||||
|
let onTap: () -> Void
|
||||||
|
let onRemove: (() -> Void)?
|
||||||
|
|
||||||
|
init(label: String, isSelected: Bool = false, isRemovable: Bool = false, onTap: @escaping () -> Void, onRemove: (() -> Void)? = nil) {
|
||||||
|
self.label = label
|
||||||
|
self.isSelected = isSelected
|
||||||
|
self.isRemovable = isRemovable
|
||||||
|
self.onTap = onTap
|
||||||
|
self.onRemove = onRemove
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: onTap) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(label)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(isSelected ? .white : .primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
if isRemovable, let onRemove = onRemove {
|
||||||
|
Button(action: onRemove) {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(isSelected ? .white.opacity(0.8) : .secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.frame(minHeight: 32)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(isSelected ? Color.accentColor : Color.accentColor.opacity(0.15))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.stroke(Color.accentColor.opacity(0.4), lineWidth: 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: "Sample Label",
|
||||||
|
isSelected: false,
|
||||||
|
isRemovable: false,
|
||||||
|
onTap: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: "Selected Label",
|
||||||
|
isSelected: true,
|
||||||
|
isRemovable: false,
|
||||||
|
onTap: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: "Removable Label",
|
||||||
|
isSelected: false,
|
||||||
|
isRemovable: true,
|
||||||
|
onTap: {},
|
||||||
|
onRemove: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
UnifiedLabelChip(
|
||||||
|
label: "Selected & Removable",
|
||||||
|
isSelected: true,
|
||||||
|
isRemovable: true,
|
||||||
|
onTap: {},
|
||||||
|
onRemove: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||