Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • videolan/vlc-ios
  • gsoc/GSoC2018/bubu/vlc-ios
  • bubu/vlc-ios
  • chamander/vlc-ios
  • fkuehne/vlc-ios
  • Yakuzzza/vlc-ios
  • nishiths23/vlc-ios
  • alexkusssha/vlc-ios
  • jay18001/vlc-ios
  • hoangduc67/vlc-ios
  • gsoc/gsoc2019/robwayne/vlc-ios
  • groschoppsteven/vlc-ios
  • tguillem/vlc-ios
  • ePirat/vlc-ios
  • cerezo074/vlc-ios
  • edrflt/vlc-ios
  • Zirkovskij/vlc-ios
  • bakroistvan/vlc-ios
  • heidlerjustin/vlc-ios
  • W1ns/vlc-ios
  • karpun.ksv122454/vlc-ios
  • adtrevor/vlc-ios
  • rrangel3584/vlc-ios
  • Ggjgg/vlc-ios
  • tanenyi/vlc-ios
  • tmsblgh/vlc-ios
  • gale4004/vlc-ios
  • abytom/vlc-ios
  • rubendelapena/vlc-ios
  • DanielaRocha6/vlc-ios
  • kuznetsov-m/vlc-ios
  • dcodesuport/vlc-ios
  • gsoc/gsoc2020/swapnanildholg/vlc-ios
  • alexandre-janniaux/vlc-ios
  • zhkzte/vlc-ios
  • elbrujo1987/vlc-ios
  • PJStation/vlc-ios
  • diogo.simao-marques/vlc-ios
  • lalo-azamar/vlc-ios
  • dnicolson/vlc-ios
  • megan30/vlc-ios
  • yreifschneider/vlc-ios
  • pushpinderpalsingh/vlc-ios
  • jfarmer/vlc-ios
  • malekBarkaoui/vlc-ios
  • m/vlc-ios
  • zuzuweasly/vlc-ios
  • beingsparsh/vlc-ios
  • anubhavsingh19/vlc-ios
  • SnailMS/vlc-ios
  • dariustancode/vlc-ios
  • chandan.309kr/vlc-ios
  • umxprime/vlc-ios
  • vlcone/vlc-ios
  • bsidhom/vlc-ios
  • DeveshBisen/vlc-ios
  • denissparrow12/vlc-ios
  • antonianemi/vlc-ios
  • fieldsmonroe433/vlc-ios
  • antonviljoen9/vlc-ios
  • greenscgea/vlc-ios
  • keyseltmelanie/vlc-ios
  • collectionbylawrencejason/vlc-ios
  • aofsurachet1983/vlc-ios
  • archi.fahim/vlc-ios
  • XuanTung95/vlc-ios
  • nasirhemed/vlc-ios
  • ke994780/vlc-ios
  • kiwiren6666/vlc-ios
  • walikelas90/vlc-ios
  • ikeuzochukwu6/vlc-ios
  • NOTAG/vlc-ios
  • tatoonorth418/vlc-ios
  • deutschkiller72/vlc-ios
  • alexnwayne/vlc-ios
  • kiku.masa.mune00/vlc-ios
  • onfire4g05/vlc-ios
  • ass1ngl33y/vlc-ios
  • berrylcm/vlc-ios
  • ugotmjke46/vlc-ios
  • lehmacdj/vlc-ios
  • Prabal/vlc-ios
  • protechq88/vlc-ios
  • KDOT2EAZY/vlc-ios
  • king7532/vlc-ios
  • uniqueunicorn333/vlc-ios
  • Againreallly/vlc-ios
  • mztea928/vlc-ios
  • pabloluna.bella/vlc-ios
  • EshanSingh-ES/vlc-ios
  • arditx02/vlc-ios
  • tomas23prenosil/vlc-ios
  • yonat/vlc-ios
  • whatsupmf09/vlc-ios
  • Perklone/vlc-ios
  • vettrecompetitive/vlc-ios
  • ojaidi.905/vlc-ios
  • reubot/vlc-ios
  • loegue1910/vlc-ios
  • jeffmarshall/vlc-ios
  • pup.ragnarok.1984/vlc-ios
  • aviwad/vlc-ios
  • ashishami2002/vlc-ios
  • Sumou/vlc-ios
  • iampratik/vlc-ios
  • Sliem/vlc-ios
  • Apeng/vlc-ios
  • ibrahimcetin/vlc-ios
  • Aperence/vlc-ios
  • Truls/vlc-ios
  • Nilsjoberl/vlc-ios
  • Naruyoko/vlc-ios
  • borisgolovnev/vlc-ios
  • christianbilodeau/vlc-ios
  • liamjwang/vlc-ios
  • surajeet310/vlc-ios
  • craig_r/vlc-ios
  • labala/vlc-ios
  • arthurnorat/vlc-ios
  • 0xfee1de4d/vlc-ios
  • harlanhaskins/vlc-ios
  • rae/vlc-ios
  • gremlinflat/vlc-ios
  • robbiedeane/vlc-ios
124 results
Show changes
Commits on Source (14)
Showing
with 1131 additions and 83 deletions
/*****************************************************************************
* PreferenceSettingTests.swift
* VLC for iOS
*****************************************************************************
* Copyright (c) 2025 VideoLAN. All rights reserved.
*
* Authors: Craig Reyenga <craig.reyenga # gmail.com>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
import XCTest
@testable import VLC
final class PreferenceSettingTests: XCTestCase {
func testDecodeAll() {
let fileURL = Bundle(for: type(of: self)).url(forResource: "PreferenceSettingTestsSuccess",
withExtension: "plist")!
let data = try! Data(contentsOf: fileURL)
let decoder = PropertyListDecoder()
let decoded = try! decoder.decode(PreferenceSettingRoot.self, from: data)
// Sample data is intentionally meant to have nothing to do with VLC.
// This way, global string searches won't bring up this test as a result unnecessarily.
let group = PreferenceSetting.Group(title: "First Section", footerText: "First")
let titleChoices = PreferenceSetting.Choices
.number([.init(title: "Zero", value: .from(integer: 0)),
.init(title: "One", value: .from(integer: 1)),
.init(title: "Two", value: .from(integer: 2)),
.init(title: "Three", value: .from(integer: 3))],
defaultValue: .zero)
let title = PreferenceSetting.Title(key: "numberOfParkingTickets",
title: "Number of Parking Tickets",
choices: titleChoices)
let multiChoices = PreferenceSetting.Choices
.string([.init(title: "Zero", value: "Zero"),
.init(title: "One", value: "One"),
.init(title: "Two", value: "Two"),
.init(title: "Three", value: "Three")],
defaultValue: "Zero")
let multi = PreferenceSetting.MultiValue(key: "numberOfSpeedingTickets",
title: "Number of Speeding Tickets",
choices: multiChoices)
let textField = PreferenceSetting.TextField(key: "name",
title: "Name",
defaultValue: "")
let toggle = PreferenceSetting.Toggle(key: "WashHandsBeforeEating",
title: "Wash Hands Before Eating",
defaultValue: true)
let custom = PreferenceSetting.Custom.helloWorld(.init(title: "Greetings Earth",
population: 8_200_000_000))
let expected: [PreferenceSetting] = [
.groupSpecifier(group),
.titleSpecifier(title),
.multiValueSpecifier(multi),
.textFieldSpecifier(textField),
.toggleSwitchSpecifier(toggle),
.custom(custom)
]
XCTAssertTrue(decoded.specifiers == expected)
}
// TODO: create tests for failure cases.
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>First Section</string>
<key>FooterText</key>
<string>First</string>
</dict>
<dict>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
<key>DefaultValue</key>
<integer>0</integer>
<key>Key</key>
<string>numberOfParkingTickets</string>
<key>Title</key>
<string>Number of Parking Tickets</string>
<key>Titles</key>
<array>
<string>Zero</string>
<string>One</string>
<string>Two</string>
<string>Three</string>
</array>
<key>Values</key>
<array>
<integer>0</integer>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>DefaultValue</key>
<string>Zero</string>
<key>Key</key>
<string>numberOfSpeedingTickets</string>
<key>Title</key>
<string>Number of Speeding Tickets</string>
<key>Titles</key>
<array>
<string>Zero</string>
<string>One</string>
<string>Two</string>
<string>Three</string>
</array>
<key>Values</key>
<array>
<string>Zero</string>
<string>One</string>
<string>Two</string>
<string>Three</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>Name</string>
<key>FooterText</key>
<string>Enter your name</string>
<key>Key</key>
<string>name</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>DefaultValue</key>
<true/>
<key>Key</key>
<string>WashHandsBeforeEating</string>
<key>Title</key>
<string>Wash Hands Before Eating</string>
</dict>
<dict>
<key>Type</key>
<string>VLCCustomSpecifier</string>
<key>Subtype</key>
<string>HelloWorld</string>
<key>Title</key>
<string>Greetings Earth</string>
<key>Population</key>
<integer>8200000000</integer>
</dict>
</array>
<key>StringsTable</key>
<string>Root</string>
</dict>
</plist>
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -80,6 +80,62 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>SETTINGS_PLAYBACK_SKIP_FORWARD</string>
<key>Key</key>
<string>playback-forward-skip-length</string>
<key>DefaultValue</key>
<integer>10</integer>
<key>Titles</key>
<array>
<string>SETTINGS_DURATION_FIVE</string>
<string>SETTINGS_DURATION_TEN</string>
<string>SETTINGS_DURATION_FIFTEEN</string>
<string>SETTINGS_DURATION_TWENTY</string>
<string>SETTINGS_DURATION_THIRTY</string>
<string>SETTINGS_DURATION_SIXTY</string>
</array>
<key>Values</key>
<array>
<integer>5</integer>
<integer>10</integer>
<integer>15</integer>
<integer>20</integer>
<integer>30</integer>
<integer>60</integer>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>SETTINGS_PLAYBACK_SKIP_BACKWARD</string>
<key>Key</key>
<string>playback-backward-skip-length</string>
<key>DefaultValue</key>
<integer>10</integer>
<key>Titles</key>
<array>
<string>SETTINGS_DURATION_FIVE</string>
<string>SETTINGS_DURATION_TEN</string>
<string>SETTINGS_DURATION_FIFTEEN</string>
<string>SETTINGS_DURATION_TWENTY</string>
<string>SETTINGS_DURATION_THIRTY</string>
<string>SETTINGS_DURATION_SIXTY</string>
</array>
<key>Values</key>
<array>
<integer>5</integer>
<integer>10</integer>
<integer>15</integer>
<integer>20</integer>
<integer>30</integer>
<integer>60</integer>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
......
......@@ -80,6 +80,62 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>SETTINGS_PLAYBACK_SKIP_FORWARD</string>
<key>Key</key>
<string>playback-forward-skip-length</string>
<key>DefaultValue</key>
<integer>10</integer>
<key>Titles</key>
<array>
<string>SETTINGS_DURATION_FIVE</string>
<string>SETTINGS_DURATION_TEN</string>
<string>SETTINGS_DURATION_FIFTEEN</string>
<string>SETTINGS_DURATION_TWENTY</string>
<string>SETTINGS_DURATION_THIRTY</string>
<string>SETTINGS_DURATION_SIXTY</string>
</array>
<key>Values</key>
<array>
<integer>5</integer>
<integer>10</integer>
<integer>15</integer>
<integer>20</integer>
<integer>30</integer>
<integer>60</integer>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>SETTINGS_PLAYBACK_SKIP_BACKWARD</string>
<key>Key</key>
<string>playback-backward-skip-length</string>
<key>DefaultValue</key>
<integer>10</integer>
<key>Titles</key>
<array>
<string>SETTINGS_DURATION_FIVE</string>
<string>SETTINGS_DURATION_TEN</string>
<string>SETTINGS_DURATION_FIFTEEN</string>
<string>SETTINGS_DURATION_TWENTY</string>
<string>SETTINGS_DURATION_THIRTY</string>
<string>SETTINGS_DURATION_SIXTY</string>
</array>
<key>Values</key>
<array>
<integer>5</integer>
<integer>10</integer>
<integer>15</integer>
<integer>20</integer>
<integer>30</integer>
<integer>60</integer>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
......
......@@ -97,9 +97,10 @@ class AboutController: UIViewController, MFMailComposeViewControllerDelegate, UI
}
private func loadWebsite() {
let webTheme = PresentationTheme.current.webEquivalentTheme
let mainBundle = Bundle.main
let textColor = PresentationTheme.current.colors.cellTextColor.toHex ?? "#000000"
let backgroundColor = PresentationTheme.current.colors.background.toHex ?? "#FFFFFF"
let textColor = webTheme.colors.cellTextColor.toHex ?? "#000000"
let backgroundColor = webTheme.colors.background.toHex ?? "#FFFFFF"
guard let bundleShortVersionString = mainBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String else {
return
}
......
/*****************************************************************************
* PreferenceSetting.swift
* VLC for iOS
*****************************************************************************
* Copyright (c) 2025 VideoLAN. All rights reserved.
*
* Authors: Craig Reyenga <craig.reyenga # gmail.com>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
/// A preference from Settings.app.
///
/// These structures are decodable from plists. Apple's schema is followed
/// closely, however, the decoder does not support everything in full.
///
/// https://developer.apple.com/library/archive/documentation/PreferenceSettings/Conceptual/SettingsApplicationSchemaReference/
enum PreferenceSetting: Equatable {
// Apple:
case groupSpecifier(Group)
case titleSpecifier(Title)
case multiValueSpecifier(MultiValue)
case textFieldSpecifier(TextField)
case toggleSwitchSpecifier(Toggle)
// not implemented yet, because we don't use them:
// case sliderSpecifier(Slider)
// case radioGroupSpecifier(RadioGroup)
// case childPaneSpecifier(ChildPane)
// Us:
case custom(Custom)
// Nobody:
case unsupported(String)
}
// - MARK: PreferenceSettingRoot
/// Container for preference specifiers
struct PreferenceSettingRoot {
let specifiers: [PreferenceSetting]
}
// - MARK: Types of Preferences
extension PreferenceSetting {
struct Group: Equatable {
let title: String
let footerText: String?
}
}
extension PreferenceSetting {
struct Title: Equatable {
let key: String
let title: String
let choices: Choices
}
}
extension PreferenceSetting {
struct MultiValue: Equatable {
let key: String
let title: String
let choices: Choices
}
}
extension PreferenceSetting {
struct TextField: Equatable {
let key: String
let title: String
let defaultValue: String
}
}
extension PreferenceSetting {
struct Toggle: Equatable {
let key: String
let title: String
let defaultValue: Bool
}
}
// - MARK: Custom Preferences
extension PreferenceSetting {
enum Custom: Equatable {
case helloWorld(HelloWorld)
}
}
extension PreferenceSetting.Custom {
/// An example of a custom preference. Don't use it.
struct HelloWorld: Equatable {
let title: String
let population: Int
}
}
// - MARK: Number
extension PreferenceSetting {
/// PropertyListDecoder has a bug where values that are explicitly declared
/// as being integer or float are actually decodable as either. There is no
/// way to distinguish them during decoding. To get around this, we use a
/// structure that provides both types of values and place the burden of
/// choice between the two on the consumers of this data.
struct Number: Equatable {
let float: Float
let integer: Int
static var zero: Number {
.init(float: 0, integer: 0)
}
static func from(integer: Int) -> Self {
.init(float: Float(integer), integer: integer)
}
}
}
// - MARK: Choices
extension PreferenceSetting {
/// Choices are pairings of titles and values; values can be boolean, numeric, or string.
enum Choices: Equatable {
case bool([BoolChoice], defaultValue: Bool)
case number([NumberChoice], defaultValue: Number)
case string([StringChoice], defaultValue: String)
}
struct BoolChoice: Equatable {
let title: String
let value: Bool
}
struct NumberChoice: Equatable {
let title: String
let value: Number
}
struct StringChoice: Equatable {
let title: String
let value: String
}
}
// - MARK: Decodable
extension PreferenceSettingRoot: Decodable {
enum CodingKeys: String, CodingKey {
case specifiers = "PreferenceSpecifiers"
}
}
extension PreferenceSetting: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeString = try container.decode(String.self, forKey: .type)
switch typeString {
case SettingType.psGroupSpecifier.rawValue:
let pref = try Group(from: decoder)
self = .groupSpecifier(pref)
case SettingType.psTitleValueSpecifier.rawValue:
let pref = try Title(from: decoder)
self = .titleSpecifier(pref)
case SettingType.psMultiValueSpecifier.rawValue:
let pref = try MultiValue(from: decoder)
self = .multiValueSpecifier(pref)
case SettingType.psTextFieldSpecifier.rawValue:
let pref = try TextField(from: decoder)
self = .textFieldSpecifier(pref)
case SettingType.psToggleSwitchSpecifier.rawValue:
let pref = try Toggle(from: decoder)
self = .toggleSwitchSpecifier(pref)
case SettingType.vlcCustomSpecifier.rawValue:
let pref = try Custom(from: decoder)
self = .custom(pref)
case SettingType.psSliderSpecifier.rawValue,
SettingType.psRadioGroupSpecifier.rawValue,
SettingType.psChildPaneSpecifier.rawValue:
fallthrough
default:
self = .unsupported(typeString)
}
}
enum CodingKeys: String, CodingKey {
case type = "Type"
}
}
extension PreferenceSetting.Group: Decodable {
enum CodingKeys: String, CodingKey {
case title = "Title"
case footerText = "FooterText"
}
}
extension PreferenceSetting.Title: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(String.self, forKey: .key)
self.title = try container.decode(String.self, forKey: .title)
self.choices = try .init(from: decoder)
}
enum CodingKeys: String, CodingKey {
case key = "Key"
case title = "Title"
}
}
extension PreferenceSetting.MultiValue: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(String.self, forKey: .key)
self.title = try container.decode(String.self, forKey: .title)
self.choices = try .init(from: decoder)
}
enum CodingKeys: String, CodingKey {
case key = "Key"
case title = "Title"
}
}
extension PreferenceSetting.TextField: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(String.self, forKey: .key)
self.title = try container.decode(String.self, forKey: .title)
self.defaultValue = try container.decodeIfPresent(String.self, forKey: .defaultValue) ?? ""
}
enum CodingKeys: String, CodingKey {
case key = "Key"
case title = "Title"
case defaultValue = "DefaultValue"
}
}
extension PreferenceSetting.Toggle: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(String.self, forKey: .key)
self.title = try container.decode(String.self, forKey: .title)
self.defaultValue = try container.decode(Bool.self, forKey: .defaultValue)
}
enum CodingKeys: String, CodingKey {
case key = "Key"
case title = "Title"
case defaultValue = "DefaultValue"
}
}
extension PreferenceSetting.Custom: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let subtype = try container.decode(String.self, forKey: .subtype)
switch subtype {
case PreferenceSetting.CustomSettingSubType.helloWorld.rawValue:
self = .helloWorld(try HelloWorld(from: decoder))
default:
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Unsupported custom setting subtype: \(subtype)"))
}
}
enum CodingKeys: String, CodingKey {
case subtype = "Subtype"
}
}
extension PreferenceSetting.Custom.HelloWorld: Decodable {
enum CodingKeys: String, CodingKey {
case title = "Title"
case population = "Population"
}
}
extension PreferenceSetting.Choices {
/// A value type used only for the purposes of decoding.
fileprivate enum IntermediateValue: Decodable {
case bool(Bool)
case string(String)
case number(PreferenceSetting.Number)
var boolValue: Bool? {
switch self {
case let .bool(value):
return value
default:
return nil
}
}
var stringValue: String? {
switch self {
case let .string(value):
return value
default:
return nil
}
}
var numberValue: PreferenceSetting.Number? {
switch self {
case let .number(value):
return value
default:
return nil
}
}
var floatValue: Float? {
switch self {
case let .number(value):
return value.float
default:
return nil
}
}
var integerValue: Int? {
switch self {
case let .number(value):
return value.integer
default:
return nil
}
}
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
return
}
if let str = try? container.decode(String.self) {
self = .string(str)
return
}
let float = try container.decode(Float.self)
let integer = try container.decode(Int.self)
self = .number(PreferenceSetting.Number(float: float, integer: integer))
}
}
enum CodingKeys: String, CodingKey {
case titles = "Titles"
case values = "Values"
case defaultValue = "DefaultValue"
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let titles = try container.decode([String].self, forKey: .titles)
let values = try container.decode([IntermediateValue].self, forKey: .values)
let defaultValue = try container.decode(IntermediateValue.self, forKey: .defaultValue)
guard !titles.isEmpty else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "empty titles array"))
}
guard titles.count == values.count else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "mismatch between titles and values"))
}
switch defaultValue {
case let .bool(boolValue):
let vals = values.compactMap(\.boolValue)
let choices = zip(titles, vals).map { t, v in
PreferenceSetting.BoolChoice(title: t, value: v)
}
guard vals.count == values.count else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "values did not all decode to the same type (bool)"))
}
guard vals.contains(boolValue) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Default value \(boolValue) not found in values"))
}
self = .bool(choices, defaultValue: boolValue)
case let .number(numberValue):
let vals = values.compactMap(\.numberValue)
let choices = zip(titles, vals).map { t, v in
PreferenceSetting.NumberChoice(title: t, value: v)
}
guard vals.count == values.count else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "values did not all decode to the same type (integer)"))
}
guard vals.contains(numberValue) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Default value \(numberValue) not found in values"))
}
self = .number(choices, defaultValue: numberValue)
case let .string(stringValue):
let vals = values.compactMap(\.stringValue)
let choices = zip(titles, vals).map { t, v in
PreferenceSetting.StringChoice(title: t, value: v)
}
guard vals.count == values.count else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "values did not all decode to the same type (string)"))
}
guard vals.contains(stringValue) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Default value \(stringValue) not found in values"))
}
self = .string(choices, defaultValue: stringValue)
}
}
}
// - MARK: SettingType
fileprivate extension PreferenceSetting {
enum SettingType: String {
case psGroupSpecifier = "PSGroupSpecifier"
case psTitleValueSpecifier = "PSTitleValueSpecifier"
case psMultiValueSpecifier = "PSMultiValueSpecifier"
case psTextFieldSpecifier = "PSTextFieldSpecifier"
case psToggleSwitchSpecifier = "PSToggleSwitchSpecifier"
case psSliderSpecifier = "PSSliderSpecifier"
case psRadioGroupSpecifier = "PSRadioGroupSpecifier"
case psChildPaneSpecifier = "PSChildPaneSpecifier"
case vlcCustomSpecifier = "VLCCustomSpecifier"
}
enum CustomSettingSubType: String {
case helloWorld = "HelloWorld"
}
}
......@@ -364,13 +364,18 @@ extension EditController: UICollectionViewDelegate {
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
guard let model = model as? CollectionModel,
model.mediaCollection is VLCMLAlbum,
let size = delegate?.editControllerGetAlbumHeaderSize(with: collectionView.frame.size.width) else {
guard let model = model as? CollectionModel else {
return .init(width: 0, height: 0)
}
return size
if model.mediaCollection is VLCMLAlbum,
let size = delegate?.editControllerGetAlbumHeaderSize(with: collectionView.frame.size.width) {
return size
} else if model.mediaCollection is VLCMLPlaylist {
return PlaylistHeader.getHeaderSize(with: collectionView.frame.size.width)
} else {
return .init(width: 0, height: 0)
}
}
}
......@@ -439,21 +444,23 @@ extension EditController: UICollectionViewDataSource {
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: AlbumHeader.headerID, for: indexPath)
guard let header = headerView as? AlbumHeader,
let collectionModel = model as? CollectionModel,
collectionModel.mediaCollection is VLCMLAlbum else {
return headerView
guard kind == UICollectionView.elementKindSectionHeader else {
return UICollectionReusableView()
}
if let currentThumbnail = delegate?.editControllerGetCurrentThumbnail() {
header.updateImage(with: currentThumbnail)
}
header.shouldDisablePlayButtons(true)
return header
if let collectionModel = model as? CollectionModel,
collectionModel.mediaCollection is VLCMLAlbum,
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: AlbumHeader.headerID, for: indexPath) as? AlbumHeader {
header.updateImage(with: delegate?.editControllerGetCurrentThumbnail())
header.shouldDisablePlayButtons(true)
return header
} else if let collectionModel = model as? CollectionModel,
collectionModel.mediaCollection is VLCMLPlaylist,
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: PlaylistHeader.headerID, for: indexPath) as? PlaylistHeader {
return header
}
return UICollectionReusableView()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
......
......@@ -76,6 +76,9 @@ class MediaCategoryViewController: UICollectionViewController, UISearchBarDelega
private weak var albumHeader: AlbumHeader?
private lazy var albumFlowLayout = AlbumHeaderLayout()
private weak var playlistHeader: PlaylistHeader?
private lazy var navItemTitle: VLCMarqueeLabel = VLCMarqueeLabel()
private var hasLaunchedBefore: Bool {
......@@ -670,6 +673,10 @@ class MediaCategoryViewController: UICollectionViewController, UISearchBarDelega
toSize = size
collectionView?.collectionViewLayout.invalidateLayout()
updateContinueWatchingConstraints()
if let playlistHeader = playlistHeader {
playlistHeader.updateAfterRotation()
}
}
// MARK: - Edit
......@@ -1322,12 +1329,17 @@ extension MediaCategoryViewController {
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
guard let model = model as? CollectionModel,
model.mediaCollection is VLCMLAlbum else {
guard let model = model as? CollectionModel else {
return .init(width: 0, height: 0)
}
return albumFlowLayout.getHeaderSize(with: collectionView.frame.size.width)
if model.mediaCollection is VLCMLAlbum {
return albumFlowLayout.getHeaderSize(with: collectionView.frame.size.width)
} else if model.mediaCollection is VLCMLPlaylist {
return PlaylistHeader.getHeaderSize(with: collectionView.frame.size.width)
} else {
return .init(width: 0, height: 0)
}
}
}
......@@ -1413,24 +1425,44 @@ extension MediaCategoryViewController {
}
}
private func setupAlbumHeaderReusableView(headerView: AlbumHeader, collection: VLCMLAlbum) -> UICollectionReusableView {
let thumbnail = collection.thumbnail()
headerView.updateImage(with: thumbnail)
headerView.collection = collection
headerView.updateThumbnailTitle(collection.title)
headerView.shouldDisablePlayButtons(false)
headerView.updateParentView(parent: view)
albumHeader = headerView
return headerView
}
private func setupPlaylistHeaderReusableView(headerView: PlaylistHeader, collection: VLCMLPlaylist) -> UICollectionReusableView {
headerView.updateImage(with: collection.thumbnail())
headerView.updateTitle(with: collection.title())
headerView.collection = collection
headerView.sortModel = model.sortModel
playlistHeader = headerView
return headerView
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: AlbumHeader.headerID, for: indexPath)
guard let header = headerView as? AlbumHeader,
let collectionModel = model as? CollectionModel,
let collection = collectionModel.mediaCollection as? VLCMLAlbum else {
return headerView
guard kind == UICollectionView.elementKindSectionHeader else {
return UICollectionReusableView()
}
let thumbnail = collectionModel.thumbnail
header.updateImage(with: thumbnail)
header.collection = collection
header.updateThumbnailTitle(collection.title)
header.shouldDisablePlayButtons(false)
header.updateParentView(parent: view)
albumHeader = header
if let collectionModel = model as? CollectionModel,
let collection = collectionModel.mediaCollection as? VLCMLAlbum,
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: AlbumHeader.headerID, for: indexPath) as? AlbumHeader {
return setupAlbumHeaderReusableView(headerView: header, collection: collection)
} else if let collectionModel = model as? CollectionModel,
let collection = collectionModel.mediaCollection as? VLCMLPlaylist,
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: PlaylistHeader.headerID, for: indexPath) as? PlaylistHeader {
return setupPlaylistHeaderReusableView(headerView: header, collection: collection)
}
return header
return UICollectionReusableView()
}
}
......@@ -1719,6 +1751,7 @@ extension MediaCategoryViewController: EditControllerDelegate {
private extension MediaCategoryViewController {
func setupCollectionView() {
collectionView.register(AlbumHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: AlbumHeader.headerID)
collectionView.register(PlaylistHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: PlaylistHeader.headerID)
if model.cellType.nibName == mediaGridCellNibIdentifier {
//GridCells are made programmatically so we register the cell class directly.
......
/*****************************************************************************
* PlaylistHeader.swift
*
* Copyright © 2025 VLC authors and VideoLAN
*
* Authors: Diogo Simao Marques <dogo@videolabs.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class PlaylistHeader: UICollectionReusableView {
// MARK: - Properties
static var headerID = "playlistHeaderID"
var sortModel: SortModel?
var collection: VLCMLPlaylist?
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 8
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
titleLabel.font = UIFont.boldSystemFont(ofSize: 20)
titleLabel.textColor = PresentationTheme.current.colors.cellTextColor
titleLabel.translatesAutoresizingMaskIntoConstraints = false
return titleLabel
}()
private lazy var buttonStackView: UIStackView = {
let buttonStackView = UIStackView()
buttonStackView.backgroundColor = PresentationTheme.current.colors.background
buttonStackView.spacing = 20
buttonStackView.distribution = .fillEqually
buttonStackView.alignment = .center
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
return buttonStackView
}()
private lazy var playAllButton: UIButton = {
let playAllButton = UIButton(type: .custom)
playAllButton.tag = 0
playAllButton.setImage(UIImage(named: "iconPlay")?.withRenderingMode(.alwaysTemplate), for: .normal)
playAllButton.clipsToBounds = true
playAllButton.tintColor = .white
playAllButton.layer.cornerRadius = 5
playAllButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
playAllButton.backgroundColor = PresentationTheme.current.colors.orangeUI
playAllButton.addTarget(self, action: #selector(handlePlay), for: .touchUpInside)
playAllButton.translatesAutoresizingMaskIntoConstraints = false
return playAllButton
}()
private lazy var playShuffleButton: UIButton = {
let playShuffleButton = UIButton(type: .custom)
playShuffleButton.tag = 1
playShuffleButton.setImage(UIImage(named: "shuffle"), for: .normal)
playShuffleButton.clipsToBounds = true
playShuffleButton.tintColor = .white
playShuffleButton.layer.cornerRadius = 5
playShuffleButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
playShuffleButton.backgroundColor = PresentationTheme.current.colors.orangeUI
playShuffleButton.addTarget(self, action: #selector(handlePlayAllShuffle), for: .touchUpInside)
playShuffleButton.translatesAutoresizingMaskIntoConstraints = false
return playShuffleButton
}()
private var playAllButtonWidthConstraint: NSLayoutConstraint?
private var shuffleButtonWidthConstraint: NSLayoutConstraint?
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = PresentationTheme.current.colors.background
addSubview(imageView)
addSubview(titleLabel)
addSubview(buttonStackView)
buttonStackView.addArrangedSubview(playAllButton)
buttonStackView.addArrangedSubview(playShuffleButton)
#if os(iOS)
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
setupLandscapeConstraint()
} else {
setupConstraints()
}
#else
setupConstraints()
#endif
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .VLCThemeDidChangeNotification, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Private methods
private func setupConstraints() {
let buttonSize: CGFloat = 50.0
playAllButton.setTitle(nil, for: .normal)
playShuffleButton.setTitle(nil, for: .normal)
playAllButtonWidthConstraint = playAllButton.widthAnchor.constraint(equalToConstant: buttonSize)
shuffleButtonWidthConstraint = playShuffleButton.widthAnchor.constraint(equalToConstant: buttonSize)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.widthAnchor.constraint(equalToConstant: 200),
imageView.heightAnchor.constraint(equalToConstant: 200),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 20),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
titleLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 15),
titleLabel.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
buttonStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15),
buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
buttonStackView.heightAnchor.constraint(equalToConstant: buttonSize),
playAllButton.heightAnchor.constraint(equalToConstant: buttonSize),
playAllButtonWidthConstraint!,
playShuffleButton.heightAnchor.constraint(equalToConstant: buttonSize),
shuffleButtonWidthConstraint!
])
}
private func setupLandscapeConstraint() {
let buttonSize: CGFloat = 50.0
playAllButton.setTitle(NSLocalizedString("PLAY_BUTTON", comment: ""), for: .normal)
playShuffleButton.setTitle(NSLocalizedString("SHUFFLE", comment: ""), for: .normal)
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 70),
imageView.widthAnchor.constraint(equalToConstant: 200),
imageView.heightAnchor.constraint(equalToConstant: 200),
titleLabel.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 50),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -80),
buttonStackView.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor),
buttonStackView.heightAnchor.constraint(equalToConstant: buttonSize),
buttonStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
playAllButton.heightAnchor.constraint(equalToConstant: buttonSize),
playShuffleButton.heightAnchor.constraint(equalToConstant: buttonSize)
])
}
private func updateConstraintsAfterRotation() {
addSubview(imageView)
addSubview(titleLabel)
addSubview(buttonStackView)
buttonStackView.addArrangedSubview(playAllButton)
buttonStackView.addArrangedSubview(playShuffleButton)
#if os(iOS)
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
setupLandscapeConstraint()
} else {
setupConstraints()
}
#else
setupConstraints()
#endif
}
private func playAll(shuffle: Bool) {
guard let playlist = collection,
let sortModel = sortModel else {
return
}
let playbackService = PlaybackService.sharedInstance()
playbackService.isShuffleMode = shuffle
let media = playlist.files(with: sortModel.currentSort, desc: sortModel.desc)
playbackService.playCollection(media)
}
// MARK: - Methods
static func getHeaderSize(with width: CGFloat) -> CGSize {
#if os(iOS)
let isLandscape: Bool = UIDevice.current.orientation.isLandscape
let headerHeight: CGFloat = isLandscape ? 250.0 : 370.0
#else
let headerHeight: CGFloat = 350.0
#endif
return CGSize(width: width, height: headerHeight)
}
func updateImage(with image: UIImage?) {
guard let image = image else {
return
}
imageView.image = image
}
func updateTitle(with title: String) {
titleLabel.text = title
}
func updateAfterRotation() {
playAllButton.removeFromSuperview()
playShuffleButton.removeFromSuperview()
imageView.removeFromSuperview()
titleLabel.removeFromSuperview()
buttonStackView.removeFromSuperview()
if let playAllButtonWidthConstraint = playAllButtonWidthConstraint,
let shuffleButtonWidthConstraint = shuffleButtonWidthConstraint {
playAllButton.removeConstraint(playAllButtonWidthConstraint)
playShuffleButton.removeConstraint(shuffleButtonWidthConstraint)
self.playAllButtonWidthConstraint = nil
self.shuffleButtonWidthConstraint = nil
}
updateConstraintsAfterRotation()
}
// MARK: - Actions
@objc private func handlePlay() {
playAll(shuffle: false)
}
@objc private func handlePlayAllShuffle() {
playAll(shuffle: true)
}
@objc private func themeDidChange() {
let colors = PresentationTheme.current.colors
backgroundColor = colors.background
titleLabel.textColor = colors.cellTextColor
buttonStackView.backgroundColor = colors.background
}
}
......@@ -51,10 +51,6 @@ class CollectionModel: MLBaseModel {
self.medialibrary = mediaService
self.mediaCollection = mediaCollection
if mediaCollection is VLCMLPlaylist {
sortModel.sortingCriteria.append(.default)
}
self.sortModel = mediaCollection.sortModel() ?? self.sortModel
var sortingCriteria: VLCMLSortingCriteria = .default
......
......@@ -149,7 +149,7 @@ extension MLBaseModel {
}
extension VLCMLObject {
static func == (lhs: VLCMLObject, rhs: VLCMLObject) -> Bool {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.identifier() == rhs.identifier()
}
}
......
......@@ -211,7 +211,7 @@ extension VLCMLPlaylist {
extension VLCMLPlaylist: MediaCollectionModel {
func sortModel() -> SortModel? {
return nil
return SortModel([.alpha, .duration, .insertionDate, .releaseDate, .fileSize, .lastPlaybackDate, .playCount, .default])
}
func files(with criteria: VLCMLSortingCriteria = .alpha,
......
......@@ -189,9 +189,10 @@ private extension MediaLibraryService {
}
private func startMediaLibrary(on path: String) {
let excludeMediaLibrary = !UserDefaults.standard.bool(forKey: kVLCSettingBackupMediaLibrary)
let includeMediaLibrary = UserDefaults.standard.bool(forKey: kVLCSettingBackupMediaLibrary)
includeInDeviceBackup(includeMediaLibrary)
let hideML = UserDefaults.standard.bool(forKey: kVLCSettingHideLibraryInFilesApp)
excludeFromDeviceBackup(excludeMediaLibrary)
hideMediaLibrary(hideML)
if UserDefaults.standard.bool(forKey: MediaLibraryService.didForceRescan) == false {
......@@ -341,7 +342,8 @@ private extension MediaLibraryService {
saveMetaData(of: mlMedia, from: player)
}
@objc func excludeFromDeviceBackup(_ exclude: Bool) {
func includeInDeviceBackup(_ include: Bool) {
let exclude = !include
if let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
var documentURL = URL(fileURLWithPath: documentPath)
isExcludingFromBackup = true
......
......@@ -418,8 +418,8 @@ extension MediaViewController {
image: UIImage(systemName: "square.grid.2x2"),
state: isGridLayout ? .on : .off,
handler: {
[unowned self] _ in
mediaCategoryViewController.handleLayoutChange(gridLayout: true)
[unowned self, weak mediaCategoryViewController] _ in
mediaCategoryViewController?.handleLayoutChange(gridLayout: true)
menuButton.menu = generateMenu(viewController: mediaCategoryViewController)
})
......@@ -427,8 +427,8 @@ extension MediaViewController {
image: UIImage(systemName: "list.bullet"),
state: isGridLayout ? .off : .on,
handler: {
[unowned self] _ in
mediaCategoryViewController.handleLayoutChange(gridLayout: false)
[unowned self, weak mediaCategoryViewController] _ in
mediaCategoryViewController?.handleLayoutChange(gridLayout: false)
menuButton.menu = generateMenu(viewController: mediaCategoryViewController)
})
......@@ -463,8 +463,8 @@ extension MediaViewController {
image: actionImage,
state: currentSort ? .on : .off,
handler: {
[unowned self] _ in
mediaCategoryViewController.executeSortAction(with: criterion,
[unowned self, weak mediaCategoryViewController] _ in
mediaCategoryViewController?.executeSortAction(with: criterion,
desc: !sortModel.desc)
menuButton.menu = generateMenu(viewController: mediaCategoryViewController)
})
......@@ -497,8 +497,8 @@ extension MediaViewController {
@available(iOS 14.0, *)
func generateHistoryMenu() -> UIMenu {
let historyAction = UIAction(title: NSLocalizedString("BUTTON_HISTORY", comment: ""),
image: UIImage(systemName: "clock.arrow.2.circlepath")) { _ in
self.handleHistory()
image: UIImage(systemName: "clock.arrow.2.circlepath")) { [weak self] _ in
self?.handleHistory()
}
historyAction.accessibilityLabel = NSLocalizedString("BUTTON_HISTORY", comment: "")
......@@ -542,8 +542,8 @@ extension MediaViewController {
let includeAllArtist = UIAction(title: NSLocalizedString("HIDE_FEAT_ARTISTS", comment: ""),
image: UIImage(systemName: "person.3"),
state: isIncludeAllArtistActive ? .on : .off,
handler: { _ in
mediaCategoryViewController.actionSheetSortSectionHeaderShouldHideFeatArtists(onSwitchIsOnChange: !isIncludeAllArtistActive)
handler: { [weak mediaCategoryViewController] _ in
mediaCategoryViewController?.actionSheetSortSectionHeaderShouldHideFeatArtists(onSwitchIsOnChange: !isIncludeAllArtistActive)
})
additionalMenuItems.append(includeAllArtist)
......@@ -553,8 +553,8 @@ extension MediaViewController {
let hideTrackNumbers = UserDefaults.standard.bool(forKey: kVLCAudioLibraryHideTrackNumbers)
let hideTrackNumbersAction = UIAction(title: NSLocalizedString("HIDE_TRACK_NUMBERS", comment: ""),
state: hideTrackNumbers ? .on : .off,
handler: { _ in
mediaCategoryViewController.actionSheetSortSectionHeaderShouldHideTrackNumbers(onSwitchIsOnChange: !hideTrackNumbers)
handler: { [weak mediaCategoryViewController] _ in
mediaCategoryViewController?.actionSheetSortSectionHeaderShouldHideTrackNumbers(onSwitchIsOnChange: !hideTrackNumbers)
})
additionalMenuItems.append(hideTrackNumbersAction)
......
......@@ -94,8 +94,9 @@
[super viewDidAppear:animated];
// The container selected has already been parsed and is empty
if (_serverBrowser.items.count == 0 &&
[(VLCNetworkServerBrowserVLCMedia *)_serverBrowser retrieveParsedStatus] == VLCMediaParsedStatusDone) {
if ([_serverBrowser isKindOfClass:[VLCNetworkServerBrowserVLCMedia class]] &&
[(VLCNetworkServerBrowserVLCMedia *)_serverBrowser retrieveParsedStatus] == VLCMediaParsedStatusDone &&
_serverBrowser.items.count == 0) {
[self stopActivityIndicator];
[self removePlayAllAction];
}
......
......@@ -417,8 +417,11 @@ NSString *const VLCLastPlaylistPlayedMedia = @"LastPlaylistPlayedMedia";
return;
}
#if !TARGET_OS_TV
VLCMLMedia * lastMedia = [VLCMLMedia mediaForPlayingMedia: _mediaPlayer.media]; //last played VLCMLMeida before playback stops
[[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackServicePlaybackWillStop object: nil userInfo: @{VLCLastPlaylistPlayedMedia: lastMedia}];
// Last played VLCMLMedia before the playback stops
VLCMLMedia *lastMedia = [VLCMLMedia mediaForPlayingMedia: _mediaPlayer.media];
if (lastMedia) {
[[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackServicePlaybackWillStop object: nil userInfo: @{VLCLastPlaylistPlayedMedia: lastMedia}];
}
#endif
if (_mediaPlayer) {
@try {
......
......@@ -666,25 +666,24 @@ class PlayerViewController: UIViewController {
let location: CGPoint = recognizer.location(in: window)
// If minimization handler not ended yet, don't detect other gestures to don't block it.
guard minimizationInitialCenter == nil else { return .none }
guard minimizationInitialCenter == nil else {
return .none
}
var panType: PlayerPanType = .none
if location.x < 2 * windowWidth / 3 {
panType = .none
guard !playbackService.currentMediaIs360Video else {
return .projection
}
var panType: PlayerPanType = .none
#if os(iOS)
if location.x < 1 * windowWidth / 3 && playerController.isBrightnessGestureEnabled {
if location.x < windowWidth / 2 && playerController.isBrightnessGestureEnabled {
panType = .brightness
}
if location.x < 3 * windowWidth / 3 && playerController.isVolumeGestureEnabled {
} else if location.x > windowWidth / 2 && playerController.isVolumeGestureEnabled {
panType = .volume
}
#endif
if playbackService.currentMediaIs360Video {
panType = .projection
}
return panType
}
......
......@@ -614,17 +614,17 @@ typedef NS_ENUM(NSInteger, VLCPlayerScanState)
}
#pragma mark -
static const NSInteger VLCJumpInterval = 10000; // 10 seconds
- (void)jumpForward
{
NSAssert(self.isSeekable, @"Tried to seek while not media is not seekable.");
VLCPlaybackService *vpc = [VLCPlaybackService sharedInstance];
NSInteger jumpInterval = [[NSUserDefaults standardUserDefaults] integerForKey:kVLCSettingPlaybackForwardSkipLength];
if (vpc.isPlaying) {
[self jumpInterval:VLCJumpInterval];
[self jumpInterval:jumpInterval];
} else {
[self scrubbingJumpInterval:VLCJumpInterval];
[self scrubbingJumpInterval:jumpInterval];
}
}
- (void)jumpBackward
......@@ -632,11 +632,12 @@ static const NSInteger VLCJumpInterval = 10000; // 10 seconds
NSAssert(self.isSeekable, @"Tried to seek while not media is not seekable.");
VLCPlaybackService *vpc = [VLCPlaybackService sharedInstance];
NSInteger jumpInterval = [[NSUserDefaults standardUserDefaults] integerForKey:kVLCSettingPlaybackBackwardSkipLength];
if (vpc.isPlaying) {
[self jumpInterval:-VLCJumpInterval];
[self jumpInterval:-jumpInterval];
} else {
[self scrubbingJumpInterval:-VLCJumpInterval];
[self scrubbingJumpInterval:-jumpInterval];
}
}
......@@ -645,15 +646,17 @@ static const NSInteger VLCJumpInterval = 10000; // 10 seconds
NSAssert(self.isSeekable, @"Tried to seek while not media is not seekable.");
NSInteger duration = [VLCPlaybackService sharedInstance].mediaDuration;
if (duration==0) {
if (duration == 0) {
return;
}
VLCPlaybackService *vpc = [VLCPlaybackService sharedInstance];
CGFloat intervalFraction = ((CGFloat)interval)/((CGFloat)duration);
CGFloat currentFraction = vpc.playbackPosition;
currentFraction += intervalFraction;
vpc.playbackPosition = currentFraction;
if (interval > 0) {
[vpc jumpForward:(int)interval];
} else {
[vpc jumpBackward:(int)-interval];
}
}
- (void)scrubbingJumpInterval:(NSInteger)interval
......
......@@ -496,7 +496,7 @@ extension SettingsController {
extension SettingsController {
func mediaLibraryBackupActivateSwitchOn(state: Bool) {
mediaLibraryService.excludeFromDeviceBackup(state)
mediaLibraryService.includeInDeviceBackup(state)
}
}
......