diff --git a/Apple TV/TrendingCountrySelectionView.swift b/Apple TV/TrendingCountrySelectionView.swift deleted file mode 100644 index ecf8fd3f..00000000 --- a/Apple TV/TrendingCountrySelectionView.swift +++ /dev/null @@ -1,27 +0,0 @@ -import SwiftUI - -struct TrendingCountrySelectionView: View { - @State private var query: String = "" - - @ObservedObject private var store = Store<[Country]>() - @Binding var selectedCountry: Country - - @Environment(\.dismiss) private var dismiss - - var body: some View { - ScrollView(.vertical) { - ForEach(store.collection) { country in - Button(country.name) { - selectedCountry = country - dismiss() - } - } - .frame(width: 800) - } - .searchable(text: $query, prompt: Text("Country name or two letter code")) - .onChange(of: query) { newQuery in - store.replace(Country.search(newQuery)) - } - .background(.thinMaterial) - } -} diff --git a/Model/Video.swift b/Model/Video.swift index c5b8d5c1..8f4056f7 100644 --- a/Model/Video.swift +++ b/Model/Video.swift @@ -3,7 +3,7 @@ import AVKit import Foundation import SwiftyJSON -struct Video: Identifiable { +struct Video: Identifiable, Equatable { let id: String var title: String var thumbnails: [Thumbnail] @@ -161,12 +161,10 @@ struct Video: Identifiable { } } - static let options = [AVURLAssetPreferPreciseDurationAndTimingKey: false] - private static func extractFormatStreams(from streams: [JSON]) -> [Stream] { streams.map { SingleAssetStream( - avAsset: AVURLAsset(url: InvidiousAPI.proxyURLForAsset($0["url"].stringValue)!, options: options), + avAsset: AVURLAsset(url: InvidiousAPI.proxyURLForAsset($0["url"].stringValue)!), resolution: Stream.Resolution.from(resolution: $0["resolution"].stringValue)!, kind: .stream, encoding: $0["encoding"].stringValue @@ -184,12 +182,16 @@ struct Video: Identifiable { return videoAssetsURLs.map { Stream( - audioAsset: AVURLAsset(url: InvidiousAPI.proxyURLForAsset(audioAssetURL!["url"].stringValue)!, options: options), - videoAsset: AVURLAsset(url: InvidiousAPI.proxyURLForAsset($0["url"].stringValue)!, options: options), + audioAsset: AVURLAsset(url: InvidiousAPI.proxyURLForAsset(audioAssetURL!["url"].stringValue)!), + videoAsset: AVURLAsset(url: InvidiousAPI.proxyURLForAsset($0["url"].stringValue)!), resolution: Stream.Resolution.from(resolution: $0["resolution"].stringValue)!, kind: .adaptive, encoding: $0["encoding"].stringValue ) } } + + static func == (lhs: Video, rhs: Video) -> Bool { + lhs.id == rhs.id + } } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 29c3f6a5..7fe2c362 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 3705B180267B4DFB00704544 /* TrendingCountrySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */; }; + 3705B180267B4DFB00704544 /* TrendingCountrySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelection.swift */; }; 3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; }; 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; }; 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; }; @@ -129,7 +129,7 @@ 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */; }; 37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07BD2698AC96003EBB87 /* Defaults */; }; 37BD07C02698AC97003EBB87 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07BF2698AC97003EBB87 /* Siesta */; }; - 37BD07C12698AD3B003EBB87 /* TrendingCountrySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */; }; + 37BD07C12698AD3B003EBB87 /* TrendingCountrySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelection.swift */; }; 37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07B42698AA4D003EBB87 /* ContentView.swift */; }; 37BD07C72698B27B003EBB87 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07C62698B27B003EBB87 /* Introspect */; }; 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C32671614700C925CA /* AppTabNavigation.swift */; }; @@ -148,7 +148,7 @@ 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; - 37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */; }; + 37C7A1DA267CACF50010EAD6 /* TrendingCountrySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelection.swift */; }; 37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; }; 37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; }; 37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1DB267CE9D90010EAD6 /* Profile.swift */; }; @@ -210,7 +210,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountrySelectionView.swift; sourceTree = ""; }; + 3705B17F267B4DFB00704544 /* TrendingCountrySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountrySelection.swift; sourceTree = ""; }; 3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = ""; }; 3711403E26B206A6005B3555 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = ""; }; 371231832683E62F0000B307 /* VideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = ""; }; @@ -412,6 +412,7 @@ 37AAF27F26737550007FC770 /* SearchView.swift */, 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, 37AAF2932674086B007FC770 /* TabSelection.swift */, + 3705B17F267B4DFB00704544 /* TrendingCountrySelection.swift */, 3714166E267A8ACC006CA35D /* TrendingView.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, 37AAF29926740A01007FC770 /* VideosListView.swift */, @@ -462,7 +463,6 @@ 37B76E95268747C900CE5671 /* OptionsView.swift */, 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */, 373CFAC52696617C003CB2C6 /* SearchOptionsView.swift */, - 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */, 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */, 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */, 37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */, @@ -758,7 +758,7 @@ 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, - 37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */, + 37C7A1DA267CACF50010EAD6 /* TrendingCountrySelection.swift in Sources */, 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */, 3711403F26B206A6005B3555 /* SearchState.swift in Sources */, @@ -851,7 +851,7 @@ 377FC7DE267A082100A6BBAF /* VideosListView.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */, 37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */, - 37BD07C12698AD3B003EBB87 /* TrendingCountrySelectionView.swift in Sources */, + 37BD07C12698AD3B003EBB87 /* TrendingCountrySelection.swift in Sources */, 3711404026B206A6005B3555 /* SearchState.swift in Sources */, 37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, @@ -925,7 +925,7 @@ 3711404126B206A6005B3555 /* SearchState.swift in Sources */, 379775952689365600DD52A8 /* Array+Next.swift in Sources */, 37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */, - 3705B180267B4DFB00704544 /* TrendingCountrySelectionView.swift in Sources */, + 3705B180267B4DFB00704544 /* TrendingCountrySelection.swift in Sources */, 373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */, 37141675267A8E10006CA35D /* Country.swift in Sources */, 373CFAC42696616C003CB2C6 /* CoverSectionRowView.swift in Sources */, diff --git a/Shared/AppSidebarNavigation.swift b/Shared/AppSidebarNavigation.swift index 69ec7847..8f92aa46 100644 --- a/Shared/AppSidebarNavigation.swift +++ b/Shared/AppSidebarNavigation.swift @@ -31,9 +31,10 @@ struct AppSidebarNavigation: View { var content: some View { NavigationView { sidebar + .frame(minWidth: 180) Text("Select section") - .frame(maxWidth: 600) + Text("Select video") } } diff --git a/Shared/Country.swift b/Shared/Country.swift index 3f30367d..3be972fb 100644 --- a/Shared/Country.swift +++ b/Shared/Country.swift @@ -1,512 +1,234 @@ // swiftlint:disable switch_case_on_newline -enum Country: String, CaseIterable, Identifiable { +enum Country: String, CaseIterable, Identifiable, Hashable { var id: String { rawValue } - case af = "AF" - case ax = "AX" - case al = "AL" + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + case dz = "DZ" - case `as` = "AS" - case ad = "AD" - case ao = "AO" - case ai = "AI" - case aq = "AQ" - case ag = "AG" case ar = "AR" - case am = "AM" - case aw = "AW" case au = "AU" case at = "AT" case az = "AZ" - case bs = "BS" case bh = "BH" case bd = "BD" - case bb = "BB" case by = "BY" case be = "BE" - case bz = "BZ" - case bj = "BJ" - case bm = "BM" - case bt = "BT" case bo = "BO" - case bq = "BQ" case ba = "BA" - case bw = "BW" - case bv = "BV" case br = "BR" - case io = "IO" - case bn = "BN" case bg = "BG" - case bf = "BF" - case bi = "BI" - case cv = "CV" - case kh = "KH" - case cm = "CM" case ca = "CA" - case ky = "KY" - case cf = "CF" - case td = "TD" case cl = "CL" - case cn = "CN" - case cx = "CX" - case cc = "CC" case co = "CO" - case km = "KM" - case cg = "CG" - case cd = "CD" - case ck = "CK" case cr = "CR" - case ci = "CI" case hr = "HR" - case cu = "CU" - case cw = "CW" case cy = "CY" case cz = "CZ" case dk = "DK" - case dj = "DJ" - case dm = "DM" case `do` = "DO" case ec = "EC" case eg = "EG" case sv = "SV" - case gq = "GQ" - case er = "ER" case ee = "EE" - case et = "ET" - case fk = "FK" - case fo = "FO" - case fj = "FJ" case fi = "FI" case fr = "FR" - case gf = "GF" - case pf = "PF" - case tf = "TF" - case ga = "GA" - case gm = "GM" case ge = "GE" case de = "DE" case gh = "GH" - case gi = "GI" case gr = "GR" - case gl = "GL" - case gd = "GD" - case gp = "GP" - case gu = "GU" case gt = "GT" - case gg = "GG" - case gn = "GN" - case gw = "GW" - case gy = "GY" - case ht = "HT" - case hm = "HM" - case va = "VA" case hn = "HN" case hk = "HK" case hu = "HU" case `is` = "IS" case `in` = "IN" case id = "ID" - case ir = "IR" case iq = "IQ" case ie = "IE" - case im = "IM" case il = "IL" case it = "IT" case jm = "JM" case jp = "JP" - case je = "JE" case jo = "JO" case kz = "KZ" case ke = "KE" - case ki = "KI" - case kp = "KP" case kr = "KR" case kw = "KW" - case kg = "KG" - case la = "LA" case lv = "LV" case lb = "LB" - case ls = "LS" - case lr = "LR" case ly = "LY" case li = "LI" case lt = "LT" case lu = "LU" - case mo = "MO" case mk = "MK" - case mg = "MG" - case mw = "MW" case my = "MY" - case mv = "MV" - case ml = "ML" case mt = "MT" - case mh = "MH" - case mq = "MQ" - case mr = "MR" - case mu = "MU" - case yt = "YT" case mx = "MX" - case fm = "FM" - case md = "MD" - case mc = "MC" - case mn = "MN" case me = "ME" - case ms = "MS" case ma = "MA" - case mz = "MZ" - case mm = "MM" - case na = "NA" - case nr = "NR" case np = "NP" case nl = "NL" - case nc = "NC" case nz = "NZ" case ni = "NI" - case ne = "NE" case ng = "NG" - case nu = "NU" - case nf = "NF" - case mp = "MP" case no = "NO" case om = "OM" case pk = "PK" - case pw = "PW" - case ps = "PS" case pa = "PA" case pg = "PG" case py = "PY" case pe = "PE" case ph = "PH" - case pn = "PN" case pl = "PL" case pt = "PT" case pr = "PR" case qa = "QA" - case re = "RE" case ro = "RO" case ru = "RU" - case rw = "RW" - case bl = "BL" - case sh = "SH" - case kn = "KN" - case lc = "LC" - case mf = "MF" - case pm = "PM" - case vc = "VC" - case ws = "WS" - case sm = "SM" - case st = "ST" case sa = "SA" case sn = "SN" case rs = "RS" - case sc = "SC" - case sl = "SL" case sg = "SG" - case sx = "SX" case sk = "SK" case si = "SI" - case sb = "SB" - case so = "SO" case za = "ZA" - case gs = "GS" - case ss = "SS" case es = "ES" case lk = "LK" - case sd = "SD" - case sr = "SR" - case sj = "SJ" - case sz = "SZ" case se = "SE" case ch = "CH" - case sy = "SY" case tw = "TW" - case tj = "TJ" case tz = "TZ" case th = "TH" - case tl = "TL" - case tg = "TG" - case tk = "TK" - case to = "TO" - case tt = "TT" case tn = "TN" case tr = "TR" - case tm = "TM" - case tc = "TC" - case tv = "TV" case ug = "UG" case ua = "UA" case ae = "AE" case gb = "GB" case us = "US" - case um = "UM" case uy = "UY" - case uz = "UZ" - case vu = "VU" case ve = "VE" case vn = "VN" - case vg = "VG" case vi = "VI" - case wf = "WF" - case eh = "EH" case ye = "YE" - case zm = "ZM" case zw = "ZW" } extension Country { var name: String { switch self { - case .af: return "Afghanistan" - case .ax: return "Åland Islands" - case .al: return "Albania" case .dz: return "Algeria" - case .as: return "American Samoa" - case .ad: return "Andorra" - case .ao: return "Angola" - case .ai: return "Anguilla" - case .aq: return "Antarctica" - case .ag: return "Antigua and Barbuda" case .ar: return "Argentina" - case .am: return "Armenia" - case .aw: return "Aruba" case .au: return "Australia" case .at: return "Austria" case .az: return "Azerbaijan" - case .bs: return "Bahamas" case .bh: return "Bahrain" case .bd: return "Bangladesh" - case .bb: return "Barbados" case .by: return "Belarus" case .be: return "Belgium" - case .bz: return "Belize" - case .bj: return "Benin" - case .bm: return "Bermuda" - case .bt: return "Bhutan" case .bo: return "Bolivia (Plurinational State of)" - case .bq: return "Bonaire, Sint Eustatius and Saba" case .ba: return "Bosnia and Herzegovina" - case .bw: return "Botswana" - case .bv: return "Bouvet Island" case .br: return "Brazil" - case .io: return "British Indian Ocean Territory" - case .bn: return "Brunei Darussalam" case .bg: return "Bulgaria" - case .bf: return "Burkina Faso" - case .bi: return "Burundi" - case .cv: return "Cabo Verde" - case .kh: return "Cambodia" - case .cm: return "Cameroon" case .ca: return "Canada" - case .ky: return "Cayman Islands" - case .cf: return "Central African Republic" - case .td: return "Chad" case .cl: return "Chile" - case .cn: return "China" - case .cx: return "Christmas Island" - case .cc: return "Cocos (Keeling) Islands" case .co: return "Colombia" - case .km: return "Comoros" - case .cg: return "Congo" - case .cd: return "Congo (Democratic Republic of the)" - case .ck: return "Cook Islands" case .cr: return "Costa Rica" - case .ci: return "Côte d'Ivoire" case .hr: return "Croatia" - case .cu: return "Cuba" - case .cw: return "Curaçao" case .cy: return "Cyprus" case .cz: return "Czechia" case .dk: return "Denmark" - case .dj: return "Djibouti" - case .dm: return "Dominica" case .do: return "Dominican Republic" case .ec: return "Ecuador" case .eg: return "Egypt" case .sv: return "El Salvador" - case .gq: return "Equatorial Guinea" - case .er: return "Eritrea" case .ee: return "Estonia" - case .et: return "Ethiopia" - case .fk: return "Falkland Islands (Malvinas)" - case .fo: return "Faroe Islands" - case .fj: return "Fiji" case .fi: return "Finland" case .fr: return "France" - case .gf: return "French Guiana" - case .pf: return "French Polynesia" - case .tf: return "French Southern Territories" - case .ga: return "Gabon" - case .gm: return "Gambia" case .ge: return "Georgia" case .de: return "Germany" case .gh: return "Ghana" - case .gi: return "Gibraltar" case .gr: return "Greece" - case .gl: return "Greenland" - case .gd: return "Grenada" - case .gp: return "Guadeloupe" - case .gu: return "Guam" case .gt: return "Guatemala" - case .gg: return "Guernsey" - case .gn: return "Guinea" - case .gw: return "Guinea-Bissau" - case .gy: return "Guyana" - case .ht: return "Haiti" - case .hm: return "Heard Island and McDonald Islands" - case .va: return "Holy See" case .hn: return "Honduras" case .hk: return "Hong Kong" case .hu: return "Hungary" case .is: return "Iceland" case .in: return "India" case .id: return "Indonesia" - case .ir: return "Iran (Islamic Republic of)" case .iq: return "Iraq" case .ie: return "Ireland" - case .im: return "Isle of Man" case .il: return "Israel" case .it: return "Italy" case .jm: return "Jamaica" case .jp: return "Japan" - case .je: return "Jersey" case .jo: return "Jordan" case .kz: return "Kazakhstan" case .ke: return "Kenya" - case .ki: return "Kiribati" - case .kp: return "Korea (Democratic People's Republic of)" case .kr: return "Korea (Republic of)" case .kw: return "Kuwait" - case .kg: return "Kyrgyzstan" - case .la: return "Lao People's Democratic Republic" case .lv: return "Latvia" case .lb: return "Lebanon" - case .ls: return "Lesotho" - case .lr: return "Liberia" case .ly: return "Libya" case .li: return "Liechtenstein" case .lt: return "Lithuania" case .lu: return "Luxembourg" - case .mo: return "Macao" case .mk: return "Macedonia (the former Yugoslav Republic of)" - case .mg: return "Madagascar" - case .mw: return "Malawi" case .my: return "Malaysia" - case .mv: return "Maldives" - case .ml: return "Mali" case .mt: return "Malta" - case .mh: return "Marshall Islands" - case .mq: return "Martinique" - case .mr: return "Mauritania" - case .mu: return "Mauritius" - case .yt: return "Mayotte" case .mx: return "Mexico" - case .fm: return "Micronesia (Federated States of)" - case .md: return "Moldova (Republic of)" - case .mc: return "Monaco" - case .mn: return "Mongolia" case .me: return "Montenegro" - case .ms: return "Montserrat" case .ma: return "Morocco" - case .mz: return "Mozambique" - case .mm: return "Myanmar" - case .na: return "Namibia" - case .nr: return "Nauru" case .np: return "Nepal" case .nl: return "Netherlands" - case .nc: return "New Caledonia" case .nz: return "New Zealand" case .ni: return "Nicaragua" - case .ne: return "Niger" case .ng: return "Nigeria" - case .nu: return "Niue" - case .nf: return "Norfolk Island" - case .mp: return "Northern Mariana Islands" case .no: return "Norway" case .om: return "Oman" case .pk: return "Pakistan" - case .pw: return "Palau" - case .ps: return "Palestine, State of" case .pa: return "Panama" case .pg: return "Papua New Guinea" case .py: return "Paraguay" case .pe: return "Peru" case .ph: return "Philippines" - case .pn: return "Pitcairn" case .pl: return "Poland" case .pt: return "Portugal" case .pr: return "Puerto Rico" case .qa: return "Qatar" - case .re: return "Réunion" case .ro: return "Romania" case .ru: return "Russian Federation" - case .rw: return "Rwanda" - case .bl: return "Saint Barthélemy" - case .sh: return "Saint Helena, Ascension and Tristan da Cunha" - case .kn: return "Saint Kitts and Nevis" - case .lc: return "Saint Lucia" - case .mf: return "Saint Martin (French part)" - case .pm: return "Saint Pierre and Miquelon" - case .vc: return "Saint Vincent and the Grenadines" - case .ws: return "Samoa" - case .sm: return "San Marino" - case .st: return "Sao Tome and Principe" case .sa: return "Saudi Arabia" case .sn: return "Senegal" case .rs: return "Serbia" - case .sc: return "Seychelles" - case .sl: return "Sierra Leone" case .sg: return "Singapore" - case .sx: return "Sint Maarten (Dutch part)" case .sk: return "Slovakia" case .si: return "Slovenia" - case .sb: return "Solomon Islands" - case .so: return "Somalia" case .za: return "South Africa" - case .gs: return "South Georgia and the South Sandwich Islands" - case .ss: return "South Sudan" case .es: return "Spain" case .lk: return "Sri Lanka" - case .sd: return "Sudan" - case .sr: return "Suriname" - case .sj: return "Svalbard and Jan Mayen" - case .sz: return "Swaziland" case .se: return "Sweden" case .ch: return "Switzerland" - case .sy: return "Syrian Arab Republic" case .tw: return "Taiwan, Province of China[a]" - case .tj: return "Tajikistan" case .tz: return "Tanzania, United Republic of" case .th: return "Thailand" - case .tl: return "Timor-Leste" - case .tg: return "Togo" - case .tk: return "Tokelau" - case .to: return "Tonga" - case .tt: return "Trinidad and Tobago" case .tn: return "Tunisia" case .tr: return "Turkey" - case .tm: return "Turkmenistan" - case .tc: return "Turks and Caicos Islands" - case .tv: return "Tuvalu" case .ug: return "Uganda" case .ua: return "Ukraine" case .ae: return "United Arab Emirates" case .gb: return "United Kingdom of Great Britain and Northern Ireland" case .us: return "United States of America" - case .um: return "United States Minor Outlying Islands" case .uy: return "Uruguay" - case .uz: return "Uzbekistan" - case .vu: return "Vanuatu" case .ve: return "Venezuela (Bolivarian Republic of)" case .vn: return "Viet Nam" - case .vg: return "Virgin Islands (British)" case .vi: return "Virgin Islands (U.S.)" - case .wf: return "Wallis and Futuna" - case .eh: return "Western Sahara" case .ye: return "Yemen" - case .zm: return "Zambia" case .zw: return "Zimbabwe" } } @@ -536,11 +258,11 @@ extension Country { } static func searchByPartialName(_ name: String) -> [Country] { - guard name.count >= 2 else { + guard !name.isEmpty else { return [] } - return filteredCountries { stringFolding($0).contains(name) } + return filteredCountries { stringFolding($0).contains(stringFolding(name)) } } private static func stringFolding(_ string: String) -> String { diff --git a/Shared/TrendingCountrySelection.swift b/Shared/TrendingCountrySelection.swift new file mode 100644 index 00000000..d4dc76ba --- /dev/null +++ b/Shared/TrendingCountrySelection.swift @@ -0,0 +1,101 @@ +import SwiftUI + +struct TrendingCountrySelection: View { + static let prompt = "Country Name or Code" + @Binding var selectedCountry: Country? + + @ObservedObject private var store = Store(Country.allCases) + + @State private var query: String = "" + @State private var selection: Country? + + @FocusState var countryIsFocused + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack { + #if os(macOS) + HStack { + TextField("Country", text: $query, prompt: Text(TrendingCountrySelection.prompt)) + .focused($countryIsFocused) + Button("Done") { selectCountryAndDismiss() } + } + .padding([.horizontal, .top]) + + countriesList + #else + NavigationView { + countriesList + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button("Done") { selectCountryAndDismiss() } + } + } + #if os(iOS) + .navigationBarTitle("Trending Country", displayMode: .automatic) + #endif + } + #endif + } + .onAppear { + countryIsFocused = true + } + .onSubmit { selectCountryAndDismiss() } + #if !os(macOS) + .searchable(text: $query, placement: searchPlacement, prompt: Text(TrendingCountrySelection.prompt)) + #endif + #if os(tvOS) + .background(.thinMaterial) + #endif + } + + var countriesList: some View { + ScrollViewReader { _ in + List(store.collection, selection: $selection) { country in + #if os(macOS) + Text(country.name) + .tag(country) + .id(country) + #else + Button(country.name) { selectCountryAndDismiss(country) } + #endif + } + .onChange(of: query) { newQuery in + let results = Country.search(newQuery) + store.replace(results) + + selection = results.first + } + } + + #if os(macOS) + .listStyle(.inset(alternatesRowBackgrounds: true)) + .padding(.bottom, 5) + + #endif + } + + var searchPlacement: SearchFieldPlacement { + #if os(iOS) + .navigationBarDrawer(displayMode: .always) + #else + .automatic + #endif + } + + func selectCountryAndDismiss(_ country: Country? = nil) { + let selected = country ?? selection + + if selected != nil { + selectedCountry = selected + } + + dismiss() + } +} + +struct TrendingCountrySelection_Previews: PreviewProvider { + static var previews: some View { + TrendingCountrySelection(selectedCountry: .constant(.pl)) + } +} diff --git a/Shared/TrendingView.swift b/Shared/TrendingView.swift index d18469a0..021a27a8 100644 --- a/Shared/TrendingView.swift +++ b/Shared/TrendingView.swift @@ -3,7 +3,7 @@ import SwiftUI struct TrendingView: View { @State private var category: TrendingCategory = .default - @State private var country: Country = .pl + @State private var country: Country! = .pl @State private var selectingCountry = false @ObservedObject private var store = Store<[Video]>() @@ -16,62 +16,107 @@ struct TrendingView: View { resource.addObserver(store) } + var toolbar: some View { + HStack { + HStack { + Text("Category") + .foregroundColor(.secondary) + + categoryButton + } + + #if os(iOS) + Spacer() + #endif + + HStack { + Text("Country") + .foregroundColor(.secondary) + + countryButton + } + } + } + var body: some View { Section { VStack(alignment: .center, spacing: 2) { #if os(tvOS) - HStack { - Text("Category") - .foregroundColor(.secondary) - - categoryButton - - Text("Country") - .foregroundColor(.secondary) - - countryFlag - countryButton - } - .scaleEffect(0.85) + toolbar + .scaleEffect(0.85) #endif VideosView(videos: store.collection) + + #if os(iOS) + toolbar + .font(.system(size: 14)) + .animation(nil) + .padding(.horizontal) + .padding(.vertical, 10) + .overlay(Divider().offset(x: 0, y: -2), alignment: .topTrailing) + #endif } } - #if !os(tvOS) + #if os(tvOS) + .fullScreenCover(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) { + TrendingCountrySelection(selectedCountry: $country) + } + #else + .sheet(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) { + TrendingCountrySelection(selectedCountry: $country) + #if os(macOS) + .frame(minWidth: 400, minHeight: 400) + #endif + } .navigationTitle("Trending") #endif + #if os(macOS) + .toolbar { + ToolbarItemGroup { + categoryButton + countryButton + } + } + #endif + .onAppear { resource.loadIfNeeded() } } var categoryButton: some View { - Button(category.name) { - setCategory(category.next()) - } - .contextMenu { - ForEach(TrendingCategory.allCases) { category in - Button(category.name) { setCategory(category) } + #if os(tvOS) + Button(category.name) { + setCategory(category.next()) } - } - } - - var countryFlag: some View { - Text(country.flag) - .font(.system(size: 60)) + .contextMenu { + ForEach(TrendingCategory.allCases) { category in + Button(category.name) { setCategory(category) } + } + } + #else + Menu(category.name) { + ForEach(TrendingCategory.allCases) { category in + Button(action: { setCategory(category) }) { + if category == self.category { + Label(category.name, systemImage: "checkmark") + } else { + Text(category.name) + } + } + } + } + #endif } var countryButton: some View { - Button(country.rawValue) { + Button(action: { selectingCountry.toggle() resource.removeObservers(ownedBy: store) + }) { + Text("\(country.flag) \(country.id)") } - #if os(tvOS) - .fullScreenCover(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) { - TrendingCountrySelectionView(selectedCountry: $country) - } - #endif } fileprivate func setCategory(_ category: TrendingCategory) { @@ -87,3 +132,10 @@ struct TrendingView: View { resource.loadIfNeeded() } } + +struct TrendingView_Previews: PreviewProvider { + static var previews: some View { + TrendingView() + .environmentObject(NavigationState()) + } +} diff --git a/Shared/VideoView.swift b/Shared/VideoView.swift index 97b6e5ed..c3235959 100644 --- a/Shared/VideoView.swift +++ b/Shared/VideoView.swift @@ -193,7 +193,7 @@ struct VideoView: View { minHeight: Double = 140, maxHeight: Double = .infinity ) -> some View { - ZStack(alignment: .trailing) { + ZStack(alignment: .leading) { thumbnail(.maxres, minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) VStack { @@ -222,6 +222,7 @@ struct VideoView: View { .padding(10) } } + .frame(maxWidth: 600) } func thumbnail( diff --git a/Shared/VideosListView.swift b/Shared/VideosListView.swift index f276bb38..9e46dd80 100644 --- a/Shared/VideosListView.swift +++ b/Shared/VideosListView.swift @@ -6,17 +6,25 @@ struct VideosListView: View { var body: some View { Section { - List { - ForEach(videos) { video in - VideoView(video: video, layout: .list) - .contextMenu { VideoContextMenuView(video: video) } - #if os(tvOS) - .listRowInsets(listRowInsets) + ScrollViewReader { scrollView in + List { + ForEach(videos) { video in + VideoView(video: video, layout: .list) + .contextMenu { VideoContextMenuView(video: video) } + #if os(tvOS) + .listRowInsets(listRowInsets) + #elseif os(iOS) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) + #endif + } + .onChange(of: videos) { videos in + guard let video = videos.first else { + return + } - #elseif os(iOS) - .listRowInsets(EdgeInsets(.zero)) - .listRowSeparator(.hidden) - #endif + scrollView.scrollTo(video.id, anchor: .top) + } } } #if os(tvOS)